多線程概念
- 線程
線程指的是:1個(gè)CPU執(zhí)行的CPU命令列為一條無(wú)分叉路徑 - 多線程
這種無(wú)分叉路徑不止一條,存在多條即為"多線程".在多線程中,1個(gè)CPU核執(zhí)行多條不同路徑上的不同命令.需要明確的是:不管CPU技術(shù)如何,基本上1個(gè)CPU核一次能夠執(zhí)行的CPU命令始終為1.使用多線程的程序可以在某一個(gè)線程和其他線程之間反復(fù)進(jìn)行上下文切換,因此,看上去好像1個(gè)CPU能夠并列的執(zhí)行多個(gè)線程一樣,而在具有多個(gè)CPU核的情況下,就能夠提供多個(gè)CPU核并行執(zhí)行多個(gè)線程的技術(shù).
- 多線程的缺點(diǎn)
多線程技術(shù)看起來(lái)非常美好,但實(shí)際上因?yàn)樯婕暗缴舷挛那袚Q,多線程執(zhí)行的效率未必比單線程快,甚至可能會(huì)慢過(guò)單線程,而且,多個(gè)線程更新相同資源會(huì)導(dǎo)致數(shù)據(jù)的不一致(數(shù)據(jù)競(jìng)爭(zhēng)),停止等待事件的線程會(huì)導(dǎo)致多個(gè)線程相互持續(xù)等待(死鎖),使用太多線程會(huì)消耗掉大量?jī)?nèi)存等問(wèn)題. - 多線程的優(yōu)點(diǎn)
盡管極易發(fā)生各種問(wèn)題,在iOS中也應(yīng)當(dāng)使用多線程編程.因?yàn)槎嗑€程編程可以保證應(yīng)用程序的響應(yīng)性能.
應(yīng)用程序在啟動(dòng)時(shí),通過(guò)最先執(zhí)行額線程("主線程")來(lái)描繪用戶界面,處理屏幕的事件.如果在該線程中進(jìn)行長(zhǎng)時(shí)間的處理,如數(shù)據(jù)庫(kù)訪問(wèn),網(wǎng)絡(luò)請(qǐng)求等,就會(huì)妨礙主線程的執(zhí)行,從而導(dǎo)致不能更新用戶界面,應(yīng)用程序的畫(huà)面長(zhǎng)時(shí)間停滯的問(wèn)題.
3.為什么要使用多線程
使用多線程編程,在執(zhí)行長(zhǎng)時(shí)間的處理時(shí),仍可保證用戶界面的響應(yīng)性能.這是我們使用多線程編程的最大好處,而且,蘋(píng)果為了簡(jiǎn)化多線程的使用,給我們提供了多種多線程技術(shù),本文主要介紹GCD結(jié)合NSoperation在開(kāi)發(fā)中的使用.
GCD的API
- Dispatch Queue
Dispatch Queue 是執(zhí)行處理的等待隊(duì)列.,應(yīng)用程序編程人員通過(guò)dispatch_async等API,在block中將要執(zhí)行的處理追加到Dispatch Queue 中,Dispatch Queue 按照追加的順序(FIFO,先進(jìn)先出)執(zhí)行處理.
Dispatch Queue 分為兩種類型
- Serial Dispatch Queue 等待現(xiàn)在執(zhí)行中處理結(jié)束
- Concurrent Dispatch Queue 不等待現(xiàn)在執(zhí)行中處理結(jié)束
<pre>
-(void)serialDispatchQueue{
//serial Dispatch queue 的創(chuàng)建
dispatch_queue_t queue = dispatch_queue_create("DC.test01", NULL);
for (int i = 0; i< 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%d\n",i);
});
}
} - (void)concurrentDispatchQueue{
//Concurrent Dispatch queue 的創(chuàng)建
dispatch_queue_t queue = dispatch_queue_create("DC.test01", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i< 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%d\n",i);
});
}
}
</pre>
上面的代碼中,分別創(chuàng)建了serial Dispatch Queue 和Concurrent Dispatch Queue.當(dāng)為serial Dispatch Queu 時(shí),因?yàn)橐却F(xiàn)在執(zhí)行的處理結(jié)束,所以首先執(zhí)行第一個(gè)任務(wù),打印0,然后順序依次執(zhí)行其他任務(wù)(這里表現(xiàn)為0,1,2,3,4,5,6,7,8,9由小到大按順序打印).系統(tǒng)只會(huì)使用一個(gè)線程.當(dāng)為Concurrent Dispatch Queu 時(shí),因?yàn)椴挥玫却F(xiàn)在執(zhí)行的處理結(jié)束,所以首先執(zhí)行第一個(gè)任務(wù),不管第一個(gè)任務(wù)執(zhí)行是否結(jié)束,都開(kāi)始執(zhí)行第二個(gè)任務(wù),不管第二個(gè)任務(wù)執(zhí)行是否結(jié)束,都開(kāi)始執(zhí)行第三個(gè)任務(wù),如此重復(fù)循環(huán).(這里表現(xiàn)為0,1,2,3,4,5,6,7,8,9的打印沒(méi)有按照有小到大的順序,是一個(gè)隨機(jī)順序).系統(tǒng)可以并行執(zhí)行多個(gè)處理,但是并行執(zhí)行處理數(shù)量取決于當(dāng)前系統(tǒng)的狀態(tài),即iOS基于Dispatch Queue 中的處理數(shù),CPU核數(shù)以及CPU負(fù)荷等當(dāng)前系統(tǒng)狀態(tài)來(lái)決定Concurrent Dispatch Queue中并行執(zhí)行的處理數(shù).
當(dāng)生成多個(gè)serial Dispatch Queue,各個(gè)serial Dispatch Queue 將并行執(zhí)行,雖然在一個(gè)Serial Dispatch queue 中同時(shí)只能后執(zhí)行一個(gè)追加處理,但是如果將處理分別追加到4個(gè)serial Dispatch queue 中,各個(gè)serial Dispatch queue 執(zhí)行1個(gè),即為同時(shí)執(zhí)行4個(gè)處理.如果生成2000個(gè)serial Dispatch queue ,那么久生成2000個(gè)線程,而不像Concurrent Dispatch queue 那樣,系統(tǒng)會(huì)根據(jù)系統(tǒng)狀態(tài)來(lái)決定執(zhí)行處理數(shù)(生成線程的個(gè)數(shù)).如果過(guò)多使用線程,就會(huì)消耗大量?jī)?nèi)存,引起大量的上下文切換,大幅降低系統(tǒng)的響應(yīng)性能.因此,只在為了避免多線程變成問(wèn)題之一---多個(gè)線程更新相同資源導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)時(shí)使用serial Dispatch queue.
- 除了使用了dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)這個(gè)方法去創(chuàng)建Dispatch Queue .實(shí)際上,不用特意生成Dispatch Queue ,我們也可以獲取系統(tǒng)提供標(biāo)準(zhǔn)的Dispatch queue.那就是 Main Dispatch Queue 和Global Dispatch queue .
- Main Dispatch Queue 實(shí)在主線程中執(zhí)行的Dispatch queue ,因?yàn)橹骶€程只有一個(gè),所以他自然是serial Dispatch queue ,追加到main Dispatch queue 的處理在主線程的RunLoop中執(zhí)行.
- Global Dispatch queue 是所有的應(yīng)用程序都能夠使用的Concurrent Dispatch Queue .沒(méi)有必要通過(guò)Dispatch_queue_creat 函數(shù)來(lái)生成.另外,Global Dispatch queue 有四個(gè)執(zhí)行優(yōu)先級(jí),分別是高優(yōu)先級(jí)(High Priority),默認(rèn)優(yōu)先級(jí)(Default Priority),低優(yōu)先級(jí)(Low Priority)和后臺(tái)優(yōu)先級(jí)(Background Priority).
<pre>
//獲取系統(tǒng)提供標(biāo)準(zhǔn)的Dispatch queue.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t globalDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t globalLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t globalbackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
</pre>
Dispatch_queue_create 函數(shù)生成的Dispatch queue 不管是serial 還是Concurrent,都使用與默認(rèn)優(yōu)先級(jí)Global Dispatch Queue 相同有限優(yōu)先級(jí)的線程,可以使用Dispatch_set_target_queue函數(shù)變更Dispatch queue 的優(yōu)先級(jí).
2.Dispatch Group
在追加到Dispatch Queue 中的多個(gè)處理全部結(jié)束后想執(zhí)行結(jié)束處理,開(kāi)發(fā)中經(jīng)常會(huì)碰到這種需求.當(dāng)只是用一個(gè)serial Dispatch queue 的時(shí)候,只要將想執(zhí)行的結(jié)果全部追加到serial Dispatch queue 中并在最后追加結(jié)束處理即可.但是在使用Concurrent Dispatch queue 或同時(shí)使用多個(gè)Dispatch queue 是.Dispatch Group就派上用場(chǎng)了.
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"complete");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blko");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk3");
});
//打印結(jié)果為 blk2 blk1 blko blk3
2016-04-23 15:00:00.260 test01[1687:171508] complete
</pre>
上面這段代碼展示了Dispatch Group的用法.Dispatch Group 可以監(jiān)視追到到Dispatch queue 中的處理的完成情況,一旦監(jiān)測(cè)到所有處理執(zhí)行結(jié)束,就將結(jié)束的處理追加到 dispatch_group_notify中指定的Dispatch queue 中執(zhí)行.
除了使用dispatch_group_async 追加處理到Dispatch queue中,還有另外一函數(shù):Dispatch_group_enter() 和Dispatch_group_leave().
2016.7.11更新:使用Dispatch_group_enter() 和Dispatch_group_leave()可以對(duì)網(wǎng)絡(luò)請(qǐng)求等異步執(zhí)行線程也執(zhí)行回調(diào)監(jiān)聽(tīng)
3.dispatch_barrier_async
在訪問(wèn)數(shù)據(jù)庫(kù)或文件時(shí),使用serial Dispatch queue 可以避免數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題.寫(xiě)入處理確實(shí)不可與其他的寫(xiě)入處理以及包含讀取的其他某些處理并行執(zhí)行,但是如果讀取處理只是與讀取處理并行執(zhí)行,那么多個(gè)并行執(zhí)行處理就不會(huì)發(fā)生問(wèn)題.也就是說(shuō),為了高效率的訪問(wèn),讀取處理追加到Concurrent Dispatch queue 中,寫(xiě)入處理在任一個(gè)讀取處理都沒(méi)有執(zhí)行的狀態(tài)下,追加到serial Dispatch queue中即可.用之前的幾個(gè)接口也可以實(shí)現(xiàn)這個(gè)功能,但是蘋(píng)果系統(tǒng)了一個(gè)非常方便解決這個(gè)問(wèn)題的接口:dispatch_barrier_async.用代碼來(lái)演示dispatch_barrier_async的使用.
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{ NSLog(@"reading1"); });
dispatch_async(queue, ^{ NSLog(@"reading2"); });
dispatch_async(queue, ^{ NSLog(@"reading3"); });
dispatch_async(queue, ^{ NSLog(@"reading4"); });
dispatch_barrier_async(queue, ^{ NSLog(@"writing1"); });
dispatch_async(queue, ^{ NSLog(@"reading5"); });
dispatch_async(queue, ^{ NSLog(@"reading6"); });
dispatch_async(queue, ^{ NSLog(@"reading7"); });
dispatch_async(queue, ^{ NSLog(@"reading8"); });
</pre>
上面的代碼打印結(jié)果為:2016-04-23 15:21:20.474 test01[1782:184574] reading1
2016-04-23 15:21:20.474 test01[1782:184575] reading2 reading3 reading4
2016-04-23 15:21:20.475 test01[1782:184635] writing1
2016-04-23 15:21:20.475 test01[1782:184574] reading5 reading6 reading7 reading8
Dispatch_barrier_async 函數(shù)會(huì)等待追加到Concurrent Dispatch Queue 上的并行執(zhí)行的處理全部結(jié)束之后,再將指定的處理追加到該Concurrent Dispatch Queue 中,然后等待由Dispatch_barrier_async 追加的處理結(jié)束后,Concurrent Dispatch Queue才恢復(fù)為一般的動(dòng)作.用下圖來(lái)表示更加明了.將Concurrent Dispatch Queue分為三段.使用Concurrent Dispatch Queue 和Dispatch_barrier_async可以實(shí)現(xiàn)高效的函數(shù)庫(kù)訪問(wèn)和文件訪問(wèn).
4.dispatch_sync
Dispatch_async 函數(shù)的async意味著非同步,就是將指定的Block非同步的追加到指定的Dispatch queue中,Dispatch_async函數(shù)不做任何等待.
Dispatch_sync 函數(shù)的sync意味著同步,就是將指定的Block同步的追加到指定的Dispatch queue中,在追加的Block結(jié)束前,Dispatch_sync函數(shù)會(huì)一直等待.
等待意味著當(dāng)前線程停止,開(kāi)發(fā)中一定要非常注意這種情況(容易引起死鎖).Dispatch_sync其實(shí)可以看做簡(jiǎn)易的Dispatch_group_wait函數(shù).一旦調(diào)用Dispatch_sync函數(shù),那么在指定的處理執(zhí)行結(jié)束之前,該函數(shù)不會(huì)返回.
<pre>
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"Hello 1");
dispatch_sync(queue, ^{
NSLog(@"Hello 2");
});
});
dispatch_sync(queue, ^{
NSLog(@"Hello 1");
});
</pre>
分析以上代碼,main Dispatch Queue 中執(zhí)行的Block 等待 main Dispatch Queue中要執(zhí)行的Block
執(zhí)行結(jié)束.引起死鎖.
5.Dispatch Semaphore
如前所述,當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時(shí),會(huì)產(chǎn)生數(shù)據(jù)不一致的情況,有時(shí)程序還會(huì)異常結(jié)束.雖然使用serial Dispatch queue和Dispatch_barrier_async 函數(shù)可以避免這類問(wèn)題,但是當(dāng)需要進(jìn)行更細(xì)粒度的排他控制時(shí).我們就需要用到Dispatch semaphone了.
Dispatch semaphore 是持有計(jì)數(shù)的信號(hào),該計(jì)數(shù)是多線程編程中的計(jì)數(shù)類型信號(hào).在Dispatch Semaphore中.使用計(jì)數(shù)類實(shí)現(xiàn)該功能,計(jì)數(shù)為0時(shí)等待,計(jì)數(shù)為1或者大于1時(shí),減去1而不等待.比較兩段代碼:
-
代碼一:
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
運(yùn)行后報(bào)錯(cuò):test01(2034,0x10c403000) malloc: *** error for object 0x7fc1e263cbb8: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
(lldb)
</pre>
該代碼使用global Dispatch queue 更新NSMutableArray,所以執(zhí)行后,有內(nèi)存錯(cuò)誤導(dǎo)致程序異常結(jié)束的概率很高.
-
代碼二:
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//生成Dispatch Semaphore ,計(jì)數(shù)初始值設(shè)定為1,保證可訪問(wèn)NSMutableArray類對(duì)象的線程同時(shí)只能有1個(gè)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{//等待Dispatch Semaphore ,一直等待.直到Dispatch Semaphore的計(jì)數(shù)值達(dá)到或者大于1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //由于Dispatch Semaphore的計(jì)數(shù)值大于或等于1,Dispatch Semaphore的計(jì)數(shù)值減1,dispatch_semaphore_wait執(zhí)行返回. //此時(shí)Dispatch Semaphore的計(jì)數(shù)值等于0.由于可訪問(wèn)NSMutableArray類對(duì)象的線程同時(shí)只能有1個(gè),因此可安全的進(jìn)行更新 [array addObject:[NSNumber numberWithInt:i]]; //排他控制處理結(jié)束,Dispatch Semaphore的計(jì)數(shù)值加1 dispatch_semaphore_signal(semaphore); });
}
NSLog(@"%lu",(unsigned long)array.count);
打印結(jié)果test01[2045:222660] 99999,更新成功
</pre>
也可以參考同步塊(synchronization block) 和NSLock的使用.
5.Dispatch_after
有時(shí)候會(huì)有這種情況,想在指定的時(shí)間后執(zhí)行處理.這時(shí)候可以考慮使用Dispatch_after.需要注意的是,Dispatch_after并不是在指定的時(shí)間后執(zhí)行處理,而只是在指定的時(shí)間追加處理到Dispatch queue.因?yàn)镸ian Dispatch queue在主線程的RunLoop中執(zhí)行,所以在比如每隔1/60秒執(zhí)行的RunLoop中,Block最快3秒后執(zhí)行,最慢在3+1/60后執(zhí)行.雖然在有嚴(yán)格時(shí)間的要求下使用Dispatch_after會(huì)出問(wèn)題,但在想大致延遲執(zhí)行處理時(shí)可以使用.
6.Dispatch_once
使用Dispatch_once來(lái)執(zhí)行只需要運(yùn)行一次的線程安全代碼,即單例模式.常用的寫(xiě)法如下:
<pre>
-
(instancetype)shareInstance{
static DCtest *shareInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[self alloc] init];
});
return shareInstance;
}
</pre>
NSOperationQueue
GCD技術(shù)確實(shí)非常棒,然而還有一種技術(shù)"NSOperationQueue",在某些情況下,使用NSOperationQueue比GCD更方便,我們也應(yīng)該熟悉了解.
- 取消某個(gè)操作
在GCD中只負(fù)責(zé)往隊(duì)列中添加任務(wù),無(wú)法取消.然而如果使用操作隊(duì)列.運(yùn)行任務(wù)之前, 可以在NSOperation對(duì)象上調(diào)用cancel方法,該方法會(huì)設(shè)置對(duì)象內(nèi)的標(biāo)志位,用以表示此任務(wù)不需執(zhí)行,不過(guò),已啟動(dòng)的任務(wù)無(wú)法取消. - 指定操作間的依賴關(guān)系
一個(gè)操作可以依賴其他多個(gè)操作.開(kāi)發(fā)者能指定操作之間的依賴體系,是指定的操作必須在另外一個(gè)操 作順利執(zhí)行完畢后方可執(zhí)行. - 指定操作的優(yōu)先級(jí)
GCD也有優(yōu)先級(jí),不過(guò)只能指定隊(duì)列的優(yōu)先級(jí),而不能指定某個(gè)操作的優(yōu)先級(jí). - 通知鍵值觀測(cè)機(jī)制監(jiān)控NSOperation對(duì)象的屬性
NSOperation對(duì)象有許多屬性都適合通過(guò)鍵值觀測(cè)機(jī)制(KVO)來(lái)監(jiān)聽(tīng),.比如可以通過(guò)isCancelled屬性來(lái)判斷任務(wù)是否已取消,也可以通過(guò)isFinished來(lái)判斷任務(wù)是否已完成.如果想在某個(gè)任務(wù)變更起狀態(tài)是得到通知,那么鍵值觀測(cè)很有用.
在多線程開(kāi)發(fā)中,我們可以結(jié)合GCD和NSOperation,來(lái)更高效的實(shí)現(xiàn)多線程編程.