本文主講多線程相關(guān)面試問題:包括GCD、NSOperation、NSThread、多線程與鎖。
一、GCD
- 同步/異步 和串行/并發(fā)
- dispatch_barrier_async 異步柵欄調(diào)用
- dispatch_group
01 異步函數(shù)+并發(fā)隊(duì)列:開啟多條線程,并發(fā)執(zhí)行任務(wù)
02 異步函數(shù)+串行隊(duì)列:開啟一條線程,串行執(zhí)行任務(wù)
03 同步函數(shù)+并發(fā)隊(duì)列:不開線程,串行執(zhí)行任務(wù)
04 同步函數(shù)+串行隊(duì)列:不開線程,串行執(zhí)行任務(wù)
05 異步函數(shù)+主隊(duì)列:不開線程,在主線程中串行執(zhí)行任務(wù)
06 同步函數(shù)+主隊(duì)列:不開線程,串行執(zhí)行任務(wù)(注意死鎖發(fā)生)
注意同步函數(shù)和異步函數(shù)在執(zhí)行順序上面的差異
1、同步/異步 和 串行/并發(fā)
同步/異步 和 串行/并發(fā)
+ 同步分配任務(wù)到串行隊(duì)列
dispatch_sync(serial_queue,{//任務(wù)});
+ 異步分配任務(wù)到串行隊(duì)列
dispatch_async(serial_queue,{//任務(wù)});
+ 同步分配任務(wù)到并發(fā)隊(duì)列
dispatch_sync(concurrent_queue,{//任務(wù)});
+ 異步分配任務(wù)到并發(fā)隊(duì)列
dispatch_async(concurrent_queue,{//任務(wù)});
(1)、同步串行 dispatch_sync(serial_queue , ^{ //任務(wù) });
同步主線程
//同步主線程 死鎖
- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
}

結(jié)果:造成死鎖 隊(duì)列引起的循環(huán)等待.
在主隊(duì)列中提交了viewDidLoad,然后又提交了block。因此在執(zhí)行viewDidLoad過程中,需要調(diào)用block,block完成之后,viewDidLoad才能繼續(xù)往下執(zhí)行,而block因?yàn)殛?duì)列先進(jìn)先出的性質(zhì)必須要等viewDidLoad執(zhí)行結(jié)果才能調(diào)用,導(dǎo)致相互等待情況,從而死鎖.
同步串行
- (void)viewDidLoad {
dispatch_sync(serialQueue, ^{
[self doSomething];
});
}

結(jié)果:順序執(zhí)行 都在主線程,不開辟新線程
(2)、同步并發(fā) dispatch_sync(concurrent_queue , ^{ //任務(wù) });
- (void)viewDidLoad {
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
答案:12345
順序執(zhí)行 都在主線程 不開辟新線程
ps:如果2 3 都是添加到同一串行隊(duì)列 就會造成死鎖 23循環(huán)等待只要同步方式提交任務(wù),無論串行還是并發(fā)都是在當(dāng)前線程執(zhí)行
dispatch_sync() 在當(dāng)前主線程執(zhí)行
(3)、異步串行 dispatch_async(serial_queue , ^{ //任務(wù) });
異步主隊(duì)列
- (void)viewDidLoad {
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
- 順序執(zhí)行,都在主線程,不開辟新線程
(4)、異步并發(fā) dispatch_async(concurrent_queue , ^{ //任務(wù) });
//騰訊面試題:
- (void)viewDidLoad {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"1");
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"3");
});
}
- (void)printLog{ NSLog(@"2"); }
- 輸出: 13
- 該題涉及知識點(diǎn)較多:GCD、線程的Runloop、performSeletor內(nèi)部實(shí)現(xiàn)
- 異步分派到全局隊(duì)列中,GCD底層所分派的線程默認(rèn)是不開啟對應(yīng)runloop的,而performSeletor:即使是延遲0秒,也是需要提交任務(wù)到runloop的邏輯,所以performSeletor方法會失效的
- performSeletor:方法要想有效執(zhí)行, 必須是方法調(diào)用所屬線程是有runloop的,沒有就會失效
2、dispatch_barrier_async()
多讀單寫方案:dispatch_barrier_async(concurrent_queue , ^{ //寫操作 });


問題:怎么利用GCD實(shí)現(xiàn)多讀單寫?或者說想要實(shí)現(xiàn)多讀單寫,怎么去實(shí)現(xiàn)?
答:
- 讀者與讀者并發(fā)(讀操作添加到并發(fā)隊(duì)列同步訪問)
- 讀者與寫者、寫者與寫者互斥 (寫操作通過dispatch_barrier_async+ 異步柵欄添加到并發(fā)隊(duì)列中)
@interface UserCenter(){
// 定義一個(gè)并發(fā)隊(duì)列
dispatch_queue_t concurrent_queue;
// 用戶數(shù)據(jù)中心, 可能多個(gè)線程需要數(shù)據(jù)訪問
NSMutableDictionary *userCenterDic;
}
@end
// 多讀單寫模型
@implementation UserCenter
- (id)init{
self = [super init];
if (self) {
// 通過宏定義 DISPATCH_QUEUE_CONCURRENT 創(chuàng)建一個(gè)并發(fā)隊(duì)列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 創(chuàng)建數(shù)據(jù)容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key {
__block id obj;
// 同步讀取指定數(shù)據(jù)
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
- (void)setObject:(id)obj forKey:(NSString *)key {
// 異步柵欄調(diào)用設(shè)置數(shù)據(jù)
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
@end
3、dispatch_group_async()
問題:
使用GCD實(shí)現(xiàn):A、B、C三個(gè)任務(wù)并發(fā),完成后執(zhí)行任務(wù)D?
答: 所有異步任務(wù)添加到并發(fā)隊(duì)列中,然后使用dispatch_group_notify函數(shù),來監(jiān)聽前面多個(gè)的任務(wù)是否完成,如果完成, 就會調(diào)用dispatch_group_notify中的block
(參考代碼實(shí)例)
二、NSOperation
需要和NSOperationQueue配合使用來實(shí)現(xiàn)多線程方案。
1、特點(diǎn):添加任務(wù)依賴、任務(wù)執(zhí)行狀態(tài)監(jiān)控、最大并發(fā)數(shù)。
2、任務(wù)執(zhí)行狀態(tài)控制:isReady、isExecuting、isFinished、isCancelled。
3、狀態(tài)監(jiān)控
- 如果只重寫
main方法,底層控制變更任務(wù)執(zhí)行完成狀態(tài),以及任務(wù)退出。 - 如果重寫
start方法,需自行控制任務(wù)狀態(tài)。
4、系統(tǒng)是怎樣移除一個(gè)isFinished=YES的NSOperation的?
答:通過KVO的方式,通知對應(yīng)的NSOprationQueue達(dá)到對NSOperation對象進(jìn)行移除。
三、NSThread
考查面試題:
1> 如何NSThread結(jié)合runloop實(shí)現(xiàn)常駐線程
2> NSThread 的 內(nèi)部實(shí)現(xiàn)機(jī)制, start方法實(shí)現(xiàn)邏輯流程
(結(jié)合 gnustep-base-1.24.9 源碼分析)

問題: NSThread啟動(dòng)流程?
答: start() ——>創(chuàng)建pthread線程——>main()——>[target performSelector:selector]——>exit()
問題: 如何通過runloop和NSThread實(shí)現(xiàn)一個(gè)常駐線程?
從下一章runloop中尋找答案
問題: NSThread執(zhí)行原理是怎樣的?
答: 實(shí)際內(nèi)部創(chuàng)建一個(gè)pthread線程 ,當(dāng)main()函數(shù)或者指定的target 的selector方法執(zhí)行結(jié)束后,系統(tǒng)會為我們進(jìn)行線程的退出管理操作. 如果需要維護(hù)一個(gè)常駐線程,需要NSThread所對應(yīng)的selector方法中維護(hù)runloop事件循環(huán)。
四、多線程與鎖
iOS中有哪些鎖?
- @synchronized
- atomic
- OSSpinLock
- NSRecursiveLock
- NSLock
- dispatch_semaphore_t
1、 @synchronized的使用場景
一般在創(chuàng)建單例的時(shí)候使用,保證在多線程環(huán)境下創(chuàng)建的對象是唯一的
2、atomic
- 修飾屬性的關(guān)鍵字
- 對被修飾的對象進(jìn)行原子操作(不負(fù)責(zé)使用,只負(fù)責(zé)賦值)
@property(atomic)NSMutableArray *array;
self.array = [NSMutableArray array]; //array賦值操作,能保證線程安全
[self.array addObject:obj]; // array使用, 不能保證線程安全,需要額外做線程安全保護(hù)
3、OSSpinLock自旋鎖
-
循環(huán)等待詢問,不釋放當(dāng)前資源 - 用于輕量級數(shù)據(jù)訪問,簡單的int值+1/-1操作
沒有具體用過,但是可以通過分析runtime源碼來學(xué)習(xí)系統(tǒng)關(guān)于OSSpinLock自旋鎖的使用情況.
4、 NSLock
- 一般用于解決細(xì)粒度的線程同步問題, 來保證各個(gè)線程互斥,進(jìn)入自己的臨界區(qū).
- (void)methodA {
[lock lock];
[self methodB];
[lock unlock];
}
- (void)methodB {
[lock lock];
//操作邏輯
[lock unlock];
}
- 該寫法重入的原因 會導(dǎo)致死鎖!
- 解決: 通過NSRecursiveLock遞歸所可以解決
5、NSRecursiveLock 遞歸鎖
- NSRecursiveLock 遞歸鎖特性: 可以重入
- (void)methodA {
[recursiveLock lock];
[self methodB];
[recursiveLock unlock];
}
- (void)methodB {
[recursiveLock lock];
//操作邏輯
[recursiveLock unlock];
}
6、dispatch_semaphore_t 信號量
dispatch_semaphore_t 信號量 也是用來實(shí)現(xiàn)線程同步, 包括對共享資源互斥訪問的信號量機(jī)制,類似于計(jì)算機(jī)專業(yè)的記錄型信號量
創(chuàng)建信號量 dispatch_semaphore_create(1)
//dispatch_semaphore_create內(nèi)部實(shí)現(xiàn) 實(shí)例化一個(gè)結(jié)構(gòu)體
struct semaphore{
int value; // 信號量的值
List<thread>; // 線程的進(jìn)程控制表pcd 或者一些其他線程的一個(gè)唯一標(biāo)識所維護(hù)的一個(gè)線程列表
}
- dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER)
信號量-1,阻塞是一個(gè)主動(dòng)行為
//dispatch_semaphore_wait() 實(shí)現(xiàn)邏輯
{
S.value = S.value - 1;
if S.value < 0 then Block(S.List); //阻塞是一個(gè)主動(dòng)行為
}
- dispatch_semaphore_signal(semaphore) 信號量+1,喚醒是一個(gè)被動(dòng)行為
//dispatch_semaphore_signal()實(shí)現(xiàn)邏輯
{
S.value = S.value + 1;
if S.value <= 0 then wakeup(S.List); //喚醒是一個(gè)被動(dòng)行為
}
五 多線程相關(guān)面試問題
1、怎樣用GCD實(shí)現(xiàn)多讀單寫?
答: dispatch_barrier_async()的使用.
- 讀者與讀者并發(fā)(讀操作添加到并發(fā)隊(duì)列同步訪問)
- 讀者與寫者、寫者與寫者互斥 (寫操作通過dispatch_barrier_async+ 異步柵欄添加到并發(fā)隊(duì)列中)
2、iOS系統(tǒng)為我們提供的幾種多線程技術(shù)各自的特點(diǎn)是怎么樣的?
答案:GCD、NSOperation、NSThread。
- GCD 用來實(shí)現(xiàn)簡單的線程同步,包括子線程的分派,包括實(shí)現(xiàn)多讀單寫場景的解決
- NSOperation及NSOperationQueue 比如AFNetworking、SDWebImage都會涉及到NSOpration,由于它的特點(diǎn)是方便我們對任務(wù)的狀態(tài)進(jìn)行控制,包括可以控制添加依賴、移除依賴
- NSThread 一般用它來實(shí)現(xiàn)一個(gè)常駐線程
3、NSOperation對象在Finished之后是咋樣從queue當(dāng)中移除掉的?
答: NSOperation對象在Finished之后, 通過KVO的方式,通知對應(yīng)的NSOprationQueue達(dá)到對NSOperation對象進(jìn)行移除。
4、你都用過哪些鎖?結(jié)合實(shí)際談?wù)勀闶窃鯓邮褂玫模?br> 答: