一碎浇、多線程的基本概念
進(jìn)程:可以理解成一個(gè)運(yùn)行中的應(yīng)用程序,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位璃俗。
線程:是進(jìn)程的基本執(zhí)行單元奴璃,一個(gè)進(jìn)程對(duì)應(yīng)多個(gè)線程。
主線程:處理UI城豁,所有更新UI的操作都必須在主線程上執(zhí)行苟穆。不要把耗時(shí)操作放在主線程,會(huì)卡界面。
多線程:在同一時(shí)刻雳旅,一個(gè)CPU只能處理1條線程跟磨,但CPU可以在多條線程之間快速的切換,只要切換的足夠快攒盈,就造成了多線程一同執(zhí)行的假象抵拘。
線程就像火車的一節(jié)車廂,進(jìn)程則是火車型豁。車廂(線程)離開火車(進(jìn)程)是無法跑動(dòng)的僵蛛,而火車(進(jìn)程)至少有一節(jié)車廂(主線程)。多線程可以看做多個(gè)車廂迎变,它的出現(xiàn)是為了提高效率充尉。
多線程是通過提高資源使用率來提高系統(tǒng)總體的效率。
我們運(yùn)用多線程的目的是:將耗時(shí)的操作放在后臺(tái)執(zhí)行衣形!
并發(fā)與并行:《并發(fā)的藝術(shù)》中大概是這樣寫的驼侠,系統(tǒng)中有多個(gè)任務(wù)同時(shí)存在可稱之為“并發(fā)”,系統(tǒng)內(nèi)有多個(gè)任務(wù)同時(shí)執(zhí)行可稱之為“并行”谆吴;并發(fā)是并行的子集倒源。比如在單核CPU系統(tǒng)上,只可能存在并發(fā)而不可能存在并行句狼。
二笋熬、線程的狀態(tài)與生命周期
下面分別闡述線程生命周期中的每一步
新建:實(shí)例化線程對(duì)象
就緒:向線程對(duì)象發(fā)送start消息,線程對(duì)象被加入可調(diào)度線程池等待CPU調(diào)度鲜锚。
運(yùn)行:CPU 負(fù)責(zé)調(diào)度可調(diào)度線程池中線程的執(zhí)行。線程執(zhí)行完成之前苫拍,狀態(tài)可能會(huì)在就緒和運(yùn)行之間來回切換芜繁。就緒和運(yùn)行之間的狀態(tài)變化由CPU負(fù)責(zé),程序員不能干預(yù)绒极。
阻塞:當(dāng)滿足某個(gè)預(yù)定條件時(shí)骏令,可以使用休眠或鎖,阻塞線程執(zhí)行垄提。sleepForTimeInterval(休眠指定時(shí)長)榔袋,sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖)铡俐。
死亡:正常死亡凰兑,線程執(zhí)行完畢。非正常死亡审丘,當(dāng)滿足某個(gè)條件后吏够,在線程內(nèi)部中止執(zhí)行/在主線程中止線程對(duì)象
還有線程的exit和cancel
[NSThread exit]:一旦強(qiáng)行終止線程,后續(xù)的所有代碼都不會(huì)被執(zhí)行。
[thread cancel]取消:并不會(huì)直接取消線程锅知,只是給線程對(duì)象添加 isCancelled 標(biāo)記播急。
三、多線程的四種解決方案
多線程的四種解決方案分別是:pthread(不常用)售睹,NSThread桩警,GCD, NSOperation昌妹。
1捶枢、NSThread
// 創(chuàng)建線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@""];
// 線程啟動(dòng),事情做完了才會(huì)死捺宗, 一個(gè)NSThread對(duì)象就代表一條線程
[thread start];
//判斷是否是主線程
[NSThread isMainThread]
優(yōu)點(diǎn):輕量級(jí)柱蟀,簡單易用,可以直接操作線程對(duì)象
缺點(diǎn): 需要自己管理線程的生命周期蚜厉、線程同步(NSLock加鎖進(jìn)行同步操作)长已。線程同步對(duì)數(shù)據(jù)的加鎖會(huì)有一定的系統(tǒng)開銷。
2昼牛、NSOperation
NSOperation是基于GCD的更高一層封裝术瓮,NSOperation需要配合NSOperationQueue來實(shí)現(xiàn)多線程。
兩種隊(duì)列
NSOperationQueue只有兩種隊(duì)列:主隊(duì)列贰健、其他隊(duì)列胞四。非主隊(duì)列(其他隊(duì)列)可以實(shí)現(xiàn)串行或并行。
主隊(duì)列的創(chuàng)建如下伶椿,主隊(duì)列上的任務(wù)是在主線程執(zhí)行的辜伟。
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
其他隊(duì)列(非主隊(duì)列)的創(chuàng)建如下,加入到‘非隊(duì)列’中的任務(wù)默認(rèn)就是并發(fā)脊另,開啟多線程导狡。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
注意:
非主隊(duì)列(其他隊(duì)列)可以實(shí)現(xiàn)串行或并行。
隊(duì)列NSOperationQueue有一個(gè)參數(shù)叫做最大并發(fā)數(shù):maxConcurrentOperationCount偎痛。
maxConcurrentOperationCount默認(rèn)為-1旱捧,直接并發(fā)執(zhí)行,所以加入到‘非隊(duì)列’中的任務(wù)默認(rèn)就是并發(fā)踩麦,開啟多線程枚赡。
當(dāng)maxConcurrentOperationCount為1時(shí),則表示不開線程谓谦,也就是串行贫橙。
當(dāng)maxConcurrentOperationCount大于1時(shí),進(jìn)行并發(fā)執(zhí)行反粥。
系統(tǒng)對(duì)最大并發(fā)數(shù)有一個(gè)限制料皇,所以即使把maxConcurrentOperationCount設(shè)置的很大谓松,系統(tǒng)也會(huì)自動(dòng)調(diào)整。所以把最大并發(fā)數(shù)設(shè)置的很大是沒有意義的践剂。
子類
NSOperation是個(gè)抽象類鬼譬,實(shí)際運(yùn)用時(shí)中需要使用它的子類有三種方式:
使用子類NSInvocationOperation
使用子類NSBlockOperation
定義繼承自NSOperation的子類,通過實(shí)現(xiàn)內(nèi)部相應(yīng)的方法來封裝任務(wù)逊脯。
- (void)testOperationQueue {
// 創(chuàng)建隊(duì)列优质,默認(rèn)并發(fā)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 創(chuàng)建操作,NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationAddOperation) object:nil];
// 創(chuàng)建操作军洼,NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"addOperation把任務(wù)添加到隊(duì)列======%@", [NSThread currentThread]);
}
}];
[queue addOperation:invocationOperation];
[queue addOperation:blockOperation];
}
- (void)invocationOperationAddOperation {
NSLog(@"invocationOperation===aaddOperation把任務(wù)添加到隊(duì)列====%@", [NSThread currentThread]);
}
運(yùn)行結(jié)果如下巩螃,可以看出,任務(wù)都是在子線程執(zhí)行的匕争,開啟了新線程避乏!
依賴屬性
它有一個(gè)依賴屬性,某一個(gè)操作(operationB)依賴于另一個(gè)操作(operationA)甘桑,只有當(dāng)operationA執(zhí)行完畢拍皮,才能執(zhí)行operationB。
[operationB addDependency:operationA];
取消/暫停/恢復(fù)操作
- (void)cancelAllOperations; 可以取消隊(duì)列的所有操作跑杭。
- (BOOL)isSuspended; 判斷隊(duì)列是否處于暫停狀態(tài)铆帽。 YES 為暫停狀態(tài),NO 為恢復(fù)狀態(tài)德谅。
- (void)setSuspended:(BOOL)b; 可設(shè)置操作的暫停和恢復(fù)爹橱,YES 代表暫停隊(duì)列,NO 代表恢復(fù)隊(duì)列窄做。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):不需要關(guān)心線程管理愧驱,數(shù)據(jù)同步的事情,可以把精力放在要執(zhí)行的操作上椭盏∽檠猓基于GCD,是對(duì)GCD 的封裝庸汗,比GCD更加面向?qū)ο蟊谷贰peration 之間添加依賴關(guān)系手报、取消一個(gè)正在執(zhí)行的 operation 蚯舱、暫停和恢復(fù) operation queue 等
缺點(diǎn): NSOperation是個(gè)抽象類,使用它必須使用它的子類
3掩蛤、GCD
GCD全稱Grand Central Dispatch枉昏,是非常常用的多線程解決方式,核心是dispatch隊(duì)列揍鸟,把需要處理的任務(wù)放到dispatch block中兄裂。GCD 管理著一個(gè)線程池句旱, 決定著dispatch block代碼塊將在哪個(gè)線程被執(zhí)行,并且對(duì)這些線程進(jìn)行管理晰奖,來緩解大量線程被創(chuàng)建的問題谈撒。
GCD的基本概念
同步(sync):任務(wù)一個(gè)接著一個(gè),前一個(gè)沒有執(zhí)行完匾南,后面不能執(zhí)行啃匿,不開線程。
異步(async):開啟多個(gè)新線程蛆楞,任務(wù)同一時(shí)間可以一起執(zhí)行溯乒。異步是多線程的代名詞
最大的區(qū)別在于,同步線程要阻塞當(dāng)前線程豹爹,必須要等待同步線程中的任務(wù)執(zhí)行完裆悄,返回以后,才能繼續(xù)執(zhí)行下一任務(wù)臂聋;而異步線程則是不用等待光稼。
阻塞一般是指:在調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起逻住。調(diào)用線程只有在得到結(jié)果之后才會(huì)被喚醒執(zhí)行后續(xù)的操作钟哥。
隊(duì)列:裝載線程任務(wù)的隊(duì)形結(jié)構(gòu)。(系統(tǒng)以先進(jìn)先出的方式調(diào)度隊(duì)列中的任務(wù)執(zhí)行)瞎访。在GCD中有兩種隊(duì)列:串行隊(duì)列和并發(fā)隊(duì)列腻贰。
并發(fā)隊(duì)列Concurrent Dispatch Queue:線程可以同時(shí)一起進(jìn)行執(zhí)行。實(shí)際上是CPU在多條線程之間快速的切換扒秸。(并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效)
串行隊(duì)列Serial Dispatch Queue:線程只能依次有序的執(zhí)行播演。
主隊(duì)列的任務(wù)一定在主線程中執(zhí)行
主線程可以執(zhí)行主隊(duì)列之外其他隊(duì)列的任務(wù).
串行與并行針對(duì)的是隊(duì)列,而同步與異步伴奥,針對(duì)的則是線程写烤。
GCD中的三種隊(duì)列類型
The main queue(主線程串行隊(duì)列): 與主線程功能相同,提交至Main queue的任務(wù)會(huì)在主線程中執(zhí)行拾徙,
Main queue 可以通過dispatch_get_main_queue()來獲取洲炊。
Global queue(全局并發(fā)隊(duì)列): 全局并發(fā)隊(duì)列由整個(gè)進(jìn)程共享,有高尼啡、中(默認(rèn))暂衡、低、后臺(tái)四個(gè)優(yōu)先級(jí)別崖瞭。
Global queue 可以通過調(diào)用dispatch_get_global_queue函數(shù)來獲瓤癯病(可以設(shè)置優(yōu)先級(jí))
全局隊(duì)列的底層是一個(gè)線程池,向全局隊(duì)列中提交的 block书聚,都會(huì)被放到這個(gè)線程池中執(zhí)行唧领,如果線程池已滿藻雌,后續(xù)再提交 block 就不會(huì)再重新創(chuàng)建線程。等待前面的任務(wù)執(zhí)行完成斩个,才會(huì)繼續(xù)執(zhí)行胯杭。如果線程池中的線程長時(shí)間不結(jié)束,后續(xù)堆積的任務(wù)會(huì)越來越多受啥,此時(shí)就會(huì)存在 APP crash的風(fēng)險(xiǎn)歉摧。
Group queue (隊(duì)列組):將多線程進(jìn)行分組,最大的好處是可獲知所有線程的完成情況腔呜。
Group queue 可以通過調(diào)用dispatch_group_create()來獲取叁温,通過dispatch_group_notify,可以直接監(jiān)聽組里所有線程完成情況。
GCD的使用
1核畴、串行同步:當(dāng)前線程順序執(zhí)行膝但;
2、串行異步:分線程順序執(zhí)行谤草;
3跟束、并發(fā)同步:當(dāng)前線程順序執(zhí)行;
4丑孩、并發(fā)異步:分線程無序執(zhí)行冀宴;
5、主隊(duì)列同步: 死鎖温学,程序崩潰略贮;
(參考)http://ios.jobbole.com/82622/
6、主隊(duì)列異步:主線程順序執(zhí)行仗岖。
開發(fā)中關(guān)于gcd的運(yùn)用:
1逃延、開發(fā)中需要在主線程上進(jìn)行UI的相關(guān)操作,通常會(huì)把一些耗時(shí)的操作放在其他線程轧拄,比如說圖片文件下載等耗時(shí)操作揽祥。
2、GCD延時(shí)執(zhí)行
當(dāng)需要等待一會(huì)再執(zhí)行一段代碼時(shí)檩电,就可以用到這個(gè)方法了:dispatch_after拄丰。
3、GCD實(shí)現(xiàn)代碼只執(zhí)行一次
使用dispatch_once能保證某段代碼在程序運(yùn)行過程中只被執(zhí)行1次俐末×习矗可以用來設(shè)計(jì)單例。
4鹅搪、GCD隊(duì)列組
異步執(zhí)行幾個(gè)耗時(shí)操作站绪,當(dāng)這幾個(gè)操作都完成之后再回到主線程進(jìn)行操作遭铺,就可以用到隊(duì)列組了丽柿。隊(duì)列組有下面幾個(gè)特點(diǎn):
所有的任務(wù)會(huì)并發(fā)的執(zhí)行(不按序)恢准。
所有的異步函數(shù)都添加到隊(duì)列中,然后再納入隊(duì)列組的監(jiān)聽范圍甫题。
使用dispatch_group_notify函數(shù)馁筐,來監(jiān)聽上面的任務(wù)是否完成,如果完成, 就會(huì)調(diào)用這個(gè)方法坠非。
優(yōu)點(diǎn):輕量級(jí)的多線程解決方法敏沉,簡單易用,效率高炎码,速度快盟迟,基于C語言,更底層更高效潦闲,自動(dòng)管理線程生命周期(創(chuàng)建線程攒菠、調(diào)度任務(wù)、銷毀線程)歉闰。
缺點(diǎn): 1辖众、使用GCD的場景如果很復(fù)雜,就有非常大的可能遇到死鎖問題和敬。
2凹炸、如果想要給任務(wù)之間添加依賴關(guān)系、取消或者暫停一個(gè)正在執(zhí)行的任務(wù)時(shí)就會(huì)變得非常棘手
四昼弟、多線程安全
其實(shí)是多個(gè)線程同時(shí)訪問一個(gè)內(nèi)存區(qū)域出現(xiàn)的安全問題啤它。
解決多線程安全問題的方法 :
一:互斥鎖@synchronized(鎖對(duì)象)
防止不同的線程同時(shí)獲取相同的鎖 。當(dāng)新線程訪問時(shí)舱痘,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼蚕键,新線程就會(huì)進(jìn)入休眠。
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要線程同步的操作1 開始");
sleep(3);
NSLog(@"需要線程同步的操作1 結(jié)束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要線程同步的操作2");
}
});
@synchronized(obj)指令使用的obj為該鎖的唯一標(biāo)識(shí)衰粹,只有當(dāng)標(biāo)識(shí)相同時(shí)锣光,才滿足互斥,如果線程2中的@synchronized(obj)改為@synchronized(self),線程2就不會(huì)被阻塞铝耻,@synchronized指令實(shí)現(xiàn)鎖的優(yōu)點(diǎn)就是我們不需要在代碼中顯式的創(chuàng)建鎖對(duì)象誊爹,便可以實(shí)現(xiàn)鎖的機(jī)制,但作為一種預(yù)防措施瓢捉,@synchronized塊會(huì)隱式的添加一個(gè)異常處理來保護(hù)代碼频丘,它會(huì)在異常拋出的時(shí)候自動(dòng)的釋放互斥鎖。
@synchronized(obj) block 會(huì)變成 objc_sync_enter 和 objc_sync_exit 的成對(duì)兒調(diào)用泡态。 調(diào)用 objc_sync_enter(obj) 時(shí)搂漠,它用 obj 內(nèi)存地址的哈希值查找合適的 SyncData,然后將其上鎖某弦。當(dāng)你調(diào)用 objc_sync_exit(obj) 時(shí)桐汤,它查找合適的 SyncData 并將其解鎖而克。
方法二:自旋鎖
加了自旋鎖,當(dāng)新線程訪問代碼時(shí)怔毛,如果發(fā)現(xiàn)有其他線程正在鎖定代碼员萍,新線程會(huì)用死循環(huán)的方式,一直等待鎖定的代碼執(zhí)行完成拣度。相當(dāng)于不停嘗試執(zhí)行代碼碎绎,比較消耗性能。
屬性修飾atomic本身就有一把自旋鎖抗果。
下面說一下屬性修飾nonatomic 和 atomic:
nonatomic 非原子屬性,同一時(shí)間可以有很多線程讀和寫
atomic 原子屬性(線程安全)筋帖,保證同一時(shí)間只有一個(gè)線程能夠?qū)懭?但是同一個(gè)時(shí)間多個(gè)線程都可以取值),atomic 本身就有一把鎖(自旋鎖)
atomic:線程安全冤馏,需要消耗大量的資源
nonatomic:非線程安全幕随,不過效率更高,一般使用nonatomic
五宿接、多線程的應(yīng)用及問題思考:
NSNotification是同步還是異步?:
1赘淮、在分線程發(fā)送一個(gè)通知
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"test" object:self userInfo:nil];
});
主線程監(jiān)聽,并進(jìn)行UI操作
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNoti) name:@"test" object:nil];
- (void)receiveNoti {
NSLog(@"isMainThread=%d", [NSThread isMainThread]);
self.view.backgroundColor = [UIColor redColor];
}
結(jié)果:
isMainThread=0
=================================================================
Main Thread Checker: UI API called on a background thread: -[UIView setBackgroundColor:]
PID: 2318, TID: 88711, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
結(jié)論:
默認(rèn)情況下睦霎,創(chuàng)建的NSNotification是同步的梢卸,但是發(fā)布通知的時(shí)候在子線程中,那么接收方法觸發(fā)的也是在子線程中副女。
如何保證UI操作放在主線程中執(zhí)行蛤高?
SDWebImage的SDWebImageCompat.h中有這樣一個(gè)宏定義,用來保證主線程操作碑幅,為什么要這樣寫戴陡?
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
在此之前見到最多的是這樣的:
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
檢查我們當(dāng)前在主線程上執(zhí)行的最簡單的方法是使用[NSThread isMainThread] - GCD缺少一個(gè)類似的方便的API來檢查我們是否在主隊(duì)列上運(yùn)行,因此許多開發(fā)人員使用了NSThread API
這在大多數(shù)情況下是有效的沟涨,直到它出現(xiàn)了異常恤批。
潛在的問題是VektorKit API正在檢查是否在主隊(duì)列上調(diào)用它,而不是檢查它在主線程上運(yùn)行裹赴。
雖然每個(gè)應(yīng)用程序都只有一個(gè)主線程喜庞,但是在這個(gè)主線程上執(zhí)行許多不同的隊(duì)列是可能的。
如果庫(如VektorKit)依賴于在主隊(duì)列上檢查執(zhí)行棋返,那么從主線程上執(zhí)行的非主隊(duì)列調(diào)用API將導(dǎo)致問題延都。也就是說,如果在主線程執(zhí)行非主隊(duì)列調(diào)度的API睛竣,而這個(gè)API需要檢查是否由主隊(duì)列上調(diào)度晰房,那么將會(huì)出現(xiàn)問題。
來源參考
SDWebImage4.0源碼探究 http://www.reibang.com/p/b8517dc833c7
iOS多線程全套:線程生命周期,多線程的四種解決方案殊者,線程安全問題与境,GCD的使用,NSOperation的使用http://www.cocoachina.com/ios/20170707/19769.html
dispatch_barrier_(a)sync柵欄函數(shù)
https://blog.csdn.net/u013046795/article/details/47057585