iOS開發(fā)中經(jīng)常要使用到多線程,在面試的時候也是經(jīng)常問到,比較常見的面試題有下面這些:
- iOS的多線程方案有哪幾種?你更傾向于哪一種?
- GCD的隊列類型。
- 說一下OperationQueue和GCD的區(qū)別,以及各自的優(yōu)勢。
- 線程安全的處理手段有哪些?
- OC你了解的鎖有哪些?在此基礎(chǔ)上進行二次提問“
1.自旋和互斥對比
2.使用以上鎖需要注意哪些?
3.用C/OC/C++,任選其一,實現(xiàn)自旋或互斥。
下面就帶著這些問題,來總結(jié)一下多線程的相關(guān)問題。
iOS中的常見多線程方案
| 技術(shù)方案 | 簡介 | 語言 | 線程聲明周期 | 使用頻率 |
|---|---|---|---|---|
| pthread | 一套通用的多線程API,適用于Unix\Linux\windows等系統(tǒng),跨平臺,可移植,使用難度大 | C | 程序員管理 | 幾乎不用 |
| NSThread | 使用更加面向?qū)ο?,簡單易用,可直接操作線程對象 | OC | 程序員管理 | 偶爾使用 |
| GCD | 旨在替代NSThread等線程技術(shù),充分利用設(shè)備的多核 | C | 自動管理 | 經(jīng)常使用 |
| NSOperation | 基于GCD,比GCD多了一些更簡單實用的功能,使用更加面向?qū)ο?/td> | OC | 自動管理 | 經(jīng)常使用 |
GCD基礎(chǔ)回顧
GCD中有兩個用來執(zhí)行任務(wù)的函數(shù):
- 用同步的方式執(zhí)行任務(wù)
即在當前線程中去做事情
dispatch_sync(dispatch_queue_t _Nonnull queue, ^{})
- 用異步的方式執(zhí)行任務(wù)
即另外開線程去做事情
dispatch_async(dispatch_queue_t _Nonnull queue, ^{})
- 并發(fā)隊列:
可以讓多個任務(wù)同時執(zhí)行(自動開啟多個線程同時執(zhí)行任務(wù))
并發(fā)功能只有在異步函數(shù)下才有效 - 串行隊列:
讓任務(wù)一個接著一個的執(zhí)行(只會開啟一條線程,一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù))
同步,異步,串行,并行
- 同步和異步的主要影響:能不能開啟新的線程
同步:在當前線程中執(zhí)行任務(wù),不具備開啟新線程的能力
異步:在新的線程中執(zhí)行任務(wù)。具備開啟新線程的能力 - 并發(fā)和和串行的主要影響:任務(wù)的執(zhí)行方法
并發(fā):多個任務(wù)同時執(zhí)行(會開啟多條線程)
串行:一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù)(只會開啟一個線程)
下面是各種隊列的執(zhí)行效果:
| 并發(fā)隊列 | 手動創(chuàng)建的串行隊列 | 主隊列 | |
|---|---|---|---|
| 同步(sync) | 沒有開啟新線程,串行執(zhí)行任務(wù) | 沒有開啟新線程,串行執(zhí)行任務(wù) | 沒有開啟新線程,串行執(zhí)行任務(wù) |
| 異步(async) | 有開啟新線程,并發(fā)執(zhí)行任務(wù) | 有開啟新線程,串行執(zhí)行任務(wù) | 沒有開啟新線程,串行執(zhí)行任務(wù) |
GCD中死鎖的問題
面試題一:以下代碼在主線程中執(zhí)行,是否會產(chǎn)生死鎖?
- (void)viewDidLoad {
[super viewDidLoad];
//問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"執(zhí)行任務(wù)2");
});
NSLog(@"執(zhí)行任務(wù)3");
}
我們運行代碼,發(fā)現(xiàn)產(chǎn)生了崩潰,說明產(chǎn)生了死鎖。下面來分析一下為什么會產(chǎn)生死鎖:
我們知道,dispatch_sync()是同步執(zhí)行,不會開辟新線程,并且要dispatch_sync()的block執(zhí)行完了才會繼續(xù)往下執(zhí)行,所以任務(wù)2是加入到了主隊列中。主隊列是串行隊列,,所以會串行執(zhí)行加入其中的任務(wù),等一個任務(wù)執(zhí)行完了再執(zhí)行另外一個,在任務(wù)2加入主隊列之前,viewDidLoad這個大任務(wù)已經(jīng)加入了主隊列,所以任務(wù)2要等ViewDIdLoad執(zhí)行完,才會執(zhí)行任務(wù)2,也就是等到任務(wù)3執(zhí)行完,才會執(zhí)行任務(wù)2,但是由于任務(wù)3在任務(wù)2后面,所以要等到任務(wù)2執(zhí)行完了,才執(zhí)行任務(wù)3。這樣就造成了任務(wù)2等任務(wù)3,任務(wù)3等任務(wù)2,造成死鎖。
面試題二:以下代碼是否會發(fā)生死鎖:
- (void)viewDidLoad {
[super viewDidLoad];
//問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"執(zhí)行任務(wù)2");
});
//
NSLog(@"執(zhí)行任務(wù)3");
}
運行程序,發(fā)現(xiàn)程序并沒有崩潰,并且產(chǎn)生打印:
2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 執(zhí)行任務(wù)3
2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 執(zhí)行任務(wù)2
為什么把同步改成異步,就不死鎖了呢?我們分析一下,由于隊列是主隊列,一定是把任務(wù)2加到主隊列中,并且在此之前viewDidLoad的任務(wù)已經(jīng)加入到了主隊列中,所以要viewDidLoad執(zhí)行完了才能執(zhí)行任務(wù)2,由于dispatch_async()是異步執(zhí)行,所以不用等到任務(wù)2執(zhí)行完了再執(zhí)行任務(wù)3,可以直接執(zhí)行任務(wù)3,任務(wù)3執(zhí)行完了,viewDidLoad也就執(zhí)行完了,也就可以執(zhí)行任務(wù)2了,所以打印的結(jié)果一定是先執(zhí)行任務(wù)3再執(zhí)行任務(wù)2。
面試題3:以下代碼是否會發(fā)生死鎖:
- (void)viewDidLoad {
[super viewDidLoad];
//問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{//1
NSLog(@"執(zhí)行任務(wù)2");
dispatch_sync(queue, ^{
NSLog(@"執(zhí)行任務(wù)3");
});
NSLog(@"執(zhí)行任務(wù)4");
});
NSLog(@"執(zhí)行任務(wù)3");
}
運行代碼,發(fā)現(xiàn)在dispatch_sync這里產(chǎn)生了崩潰,打印結(jié)果如下:
2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 執(zhí)行任務(wù)3
2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 執(zhí)行任務(wù)2
下面分析一下為什么會產(chǎn)生死鎖:
dispatch_async是異步執(zhí)行,所以先打印了執(zhí)行任務(wù)3,然后把1這個block加入了串行隊列中,這時串行隊列中有1這個block,然后又向串行隊列中加入了任務(wù)3,任務(wù)3需要同步執(zhí)行,所以任務(wù)3執(zhí)行完了才會執(zhí)行任務(wù)4,由于串行隊列中1這個block排在任務(wù)3前面,所以要1這個block完成才會執(zhí)行任務(wù)3,也就是要任務(wù)4執(zhí)行完了才會執(zhí)行任務(wù)3,而任務(wù)4又要等到任務(wù)3執(zhí)行完成,這樣互相等待,造成死鎖。
總結(jié)
造成死鎖的條件1是同步,2是往當前的串行隊列中添加事件。
直接獲取全局隊列和手動創(chuàng)建隊列的關(guān)系
看下面一段代碼:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue4 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue5 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"%p, %p, %p, %p, %p", queue1, queue2, queue3, queue4, queue5);
打印結(jié)果:
0x1062e2500, 0x1062e2500, 0x1062e2680, 0x600000146bf0, 0x600000146ca0
queue1,queue2,queue3是直接獲取的全局隊列,從打印結(jié)果可以看出,如果優(yōu)先級相同,則獲取的是同一個隊列,如果優(yōu)先級不同,則獲取的是不同的隊列。queue4,queue5是手動創(chuàng)建的隊列,即便它們的identifier相同,但是仍然創(chuàng)建了不同的隊列。
面試題
面試題一:問下列代碼的打印結(jié)果:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue1, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
- (void)test{
NSLog(@"2");
}
我們先看一下打印結(jié)果:
1
3
很奇怪,2壓根就沒有打印,也就是壓根就沒有執(zhí)行test方法,這是為什么呢?
我們把
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
改成
[self performSelector:@selector(test) withObject:nil];
看看打印結(jié)果:
1
2
3
說明這樣是能成功執(zhí)行test函數(shù)的,我們在runtime中找一下- (id)performSelector:(SEL)aSelector withObject:(id)object;的源碼:
- (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
可以看到,就是簡單的調(diào)用objc_msgSend()。但是在runtime的源碼中卻沒有找到- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;的實現(xiàn)。
我們再修改一下代碼,讓其直接在主線程中執(zhí)行:
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
打印結(jié)果:
1
2
3
那么就有理由猜測NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");的執(zhí)行和線程有關(guān)。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];這句代碼的本質(zhì)是往runloop中去添加一個NSTimer,由于主線程中有runloop,所以可以正常執(zhí)行,但是在子線程中默認是沒有啟動runloop的,所以NSTimer也就沒有辦法成功執(zhí)行。
我們可以啟動子線程中的runloop試一下:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue1, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
打印結(jié)果:
1
3
2
GNUstep
Foundation框架是不開源的,所以我們想看其中的源碼是看不到的。GNUstep是GNU計劃的項目之一,它將Cocoa的OC庫重新開源實現(xiàn)了一遍,雖然它不是蘋果官方的完整實現(xiàn),但是和官方的實現(xiàn)十分接近,區(qū)別不大,在學習的時候我們可以用來作為參考。
源碼地址:http://www.gnustep.org/resources/downloads.php
我們下載了GNUstep的代碼后,打開找到Foundation文件夾,在這個文件夾下找到NSRunLoop.m這個文件,在這個文件中找到- (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds這個方法的實現(xiàn):
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
GSTimedPerformer *item;
item = [[GSTimedPerformer alloc] initWithSelector: aSelector
target: self
argument: argument
delay: seconds];
[[loop _timedPerformers] addObject: item];
RELEASE(item);
[loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}
GCD隊列組
思考:如何用gcd實現(xiàn)以下功能:
異步并發(fā)執(zhí)行任務(wù)1,任務(wù)2
等任務(wù)1,任務(wù)2都執(zhí)行完畢后,再回到主線程執(zhí)行任務(wù)3
我們可以用dispatch_group_t和dispatch_group_notify來完成這個功能:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
}
});
如果需要在任務(wù)1和任務(wù)2完成之后再完成任務(wù)3,任務(wù)4,可以這樣:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)4-%@", [NSThread currentThread]);
}
});