多線程概念
- 線程
線程指的是:1個CPU執(zhí)行的CPU命令列為一條無分叉路徑 - 多線程
這種無分叉路徑不止一條,存在多條即為"多線程".在多線程中,1個CPU核執(zhí)行多條不同路徑上的不同命令.需要明確的是:不管CPU技術(shù)如何,基本上1個CPU核一次能夠執(zhí)行的CPU命令始終為1.使用多線程的程序可以在某一個線程和其他線程之間反復進行上下文切換,因此,看上去好像1個CPU能夠并列的執(zhí)行多個線程一樣,而在具有多個CPU核的情況下,就能夠提供多個CPU核并行執(zhí)行多個線程的技術(shù).
- 多線程的缺點
多線程技術(shù)看起來非常美好,但實際上因為涉及到上下文切換,多線程執(zhí)行的效率未必比單線程快,甚至可能會慢過單線程,而且,多個線程更新相同資源會導致數(shù)據(jù)的不一致(數(shù)據(jù)競爭),停止等待事件的線程會導致多個線程相互持續(xù)等待(死鎖),使用太多線程會消耗掉大量內(nèi)存等問題. - 多線程的優(yōu)點
盡管極易發(fā)生各種問題,在iOS中也應當使用多線程編程.因為多線程編程可以保證應用程序的響應性能.
應用程序在啟動時,通過最先執(zhí)行額線程("主線程")來描繪用戶界面,處理屏幕的事件.如果在該線程中進行長時間的處理,如數(shù)據(jù)庫訪問,網(wǎng)絡請求等,就會妨礙主線程的執(zhí)行,從而導致不能更新用戶界面,應用程序的畫面長時間停滯的問題.
3.為什么要使用多線程
使用多線程編程,在執(zhí)行長時間的處理時,仍可保證用戶界面的響應性能.這是我們使用多線程編程的最大好處,而且,蘋果為了簡化多線程的使用,給我們提供了多種多線程技術(shù),本文主要介紹GCD結(jié)合NSoperation在開發(fā)中的使用.
GCD的API
- Dispatch Queue
Dispatch Queue 是執(zhí)行處理的等待隊列.,應用程序編程人員通過dispatch_async等API,在block中將要執(zhí)行的處理追加到Dispatch Queue 中,Dispatch Queue 按照追加的順序(FIFO,先進先出)執(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.當為serial Dispatch Queu 時,因為要等待現(xiàn)在執(zhí)行的處理結(jié)束,所以首先執(zhí)行第一個任務,打印0,然后順序依次執(zhí)行其他任務(這里表現(xiàn)為0,1,2,3,4,5,6,7,8,9由小到大按順序打印).系統(tǒng)只會使用一個線程.當為Concurrent Dispatch Queu 時,因為不用等待現(xiàn)在執(zhí)行的處理結(jié)束,所以首先執(zhí)行第一個任務,不管第一個任務執(zhí)行是否結(jié)束,都開始執(zhí)行第二個任務,不管第二個任務執(zhí)行是否結(jié)束,都開始執(zhí)行第三個任務,如此重復循環(huán).(這里表現(xiàn)為0,1,2,3,4,5,6,7,8,9的打印沒有按照有小到大的順序,是一個隨機順序).系統(tǒng)可以并行執(zhí)行多個處理,但是并行執(zhí)行處理數(shù)量取決于當前系統(tǒng)的狀態(tài),即iOS基于Dispatch Queue 中的處理數(shù),CPU核數(shù)以及CPU負荷等當前系統(tǒng)狀態(tài)來決定Concurrent Dispatch Queue中并行執(zhí)行的處理數(shù).
當生成多個serial Dispatch Queue,各個serial Dispatch Queue 將并行執(zhí)行,雖然在一個Serial Dispatch queue 中同時只能后執(zhí)行一個追加處理,但是如果將處理分別追加到4個serial Dispatch queue 中,各個serial Dispatch queue 執(zhí)行1個,即為同時執(zhí)行4個處理.如果生成2000個serial Dispatch queue ,那么久生成2000個線程,而不像Concurrent Dispatch queue 那樣,系統(tǒng)會根據(jù)系統(tǒng)狀態(tài)來決定執(zhí)行處理數(shù)(生成線程的個數(shù)).如果過多使用線程,就會消耗大量內(nèi)存,引起大量的上下文切換,大幅降低系統(tǒng)的響應性能.因此,只在為了避免多線程變成問題之一---多個線程更新相同資源導致數(shù)據(jù)競爭時使用serial Dispatch queue.
- 除了使用了dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)這個方法去創(chuàng)建Dispatch Queue .實際上,不用特意生成Dispatch Queue ,我們也可以獲取系統(tǒng)提供標準的Dispatch queue.那就是 Main Dispatch Queue 和Global Dispatch queue .
- Main Dispatch Queue 實在主線程中執(zhí)行的Dispatch queue ,因為主線程只有一個,所以他自然是serial Dispatch queue ,追加到main Dispatch queue 的處理在主線程的RunLoop中執(zhí)行.
- Global Dispatch queue 是所有的應用程序都能夠使用的Concurrent Dispatch Queue .沒有必要通過Dispatch_queue_creat 函數(shù)來生成.另外,Global Dispatch queue 有四個執(zhí)行優(yōu)先級,分別是高優(yōu)先級(High Priority),默認優(yōu)先級(Default Priority),低優(yōu)先級(Low Priority)和后臺優(yōu)先級(Background Priority).
<pre>
//獲取系統(tǒng)提供標準的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,都使用與默認優(yōu)先級Global Dispatch Queue 相同有限優(yōu)先級的線程,可以使用Dispatch_set_target_queue函數(shù)變更Dispatch queue 的優(yōu)先級.
2.Dispatch Group
在追加到Dispatch Queue 中的多個處理全部結(jié)束后想執(zhí)行結(jié)束處理,開發(fā)中經(jīng)常會碰到這種需求.當只是用一個serial Dispatch queue 的時候,只要將想執(zhí)行的結(jié)果全部追加到serial Dispatch queue 中并在最后追加結(jié)束處理即可.但是在使用Concurrent Dispatch queue 或同時使用多個Dispatch queue 是.Dispatch Group就派上用場了.
<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)測到所有處理執(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()可以對網(wǎng)絡請求等異步執(zhí)行線程也執(zhí)行回調(diào)監(jiān)聽
3.dispatch_barrier_async
在訪問數(shù)據(jù)庫或文件時,使用serial Dispatch queue 可以避免數(shù)據(jù)競爭問題.寫入處理確實不可與其他的寫入處理以及包含讀取的其他某些處理并行執(zhí)行,但是如果讀取處理只是與讀取處理并行執(zhí)行,那么多個并行執(zhí)行處理就不會發(fā)生問題.也就是說,為了高效率的訪問,讀取處理追加到Concurrent Dispatch queue 中,寫入處理在任一個讀取處理都沒有執(zhí)行的狀態(tài)下,追加到serial Dispatch queue中即可.用之前的幾個接口也可以實現(xiàn)這個功能,但是蘋果系統(tǒng)了一個非常方便解決這個問題的接口:dispatch_barrier_async.用代碼來演示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ù)會等待追加到Concurrent Dispatch Queue 上的并行執(zhí)行的處理全部結(jié)束之后,再將指定的處理追加到該Concurrent Dispatch Queue 中,然后等待由Dispatch_barrier_async 追加的處理結(jié)束后,Concurrent Dispatch Queue才恢復為一般的動作.用下圖來表示更加明了.將Concurrent Dispatch Queue分為三段.使用Concurrent Dispatch Queue 和Dispatch_barrier_async可以實現(xiàn)高效的函數(shù)庫訪問和文件訪問.
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ù)會一直等待.
等待意味著當前線程停止,開發(fā)中一定要非常注意這種情況(容易引起死鎖).Dispatch_sync其實可以看做簡易的Dispatch_group_wait函數(shù).一旦調(diào)用Dispatch_sync函數(shù),那么在指定的處理執(zhí)行結(jié)束之前,該函數(shù)不會返回.
<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
如前所述,當并行執(zhí)行的處理更新數(shù)據(jù)時,會產(chǎn)生數(shù)據(jù)不一致的情況,有時程序還會異常結(jié)束.雖然使用serial Dispatch queue和Dispatch_barrier_async 函數(shù)可以避免這類問題,但是當需要進行更細粒度的排他控制時.我們就需要用到Dispatch semaphone了.
Dispatch semaphore 是持有計數(shù)的信號,該計數(shù)是多線程編程中的計數(shù)類型信號.在Dispatch Semaphore中.使用計數(shù)類實現(xiàn)該功能,計數(shù)為0時等待,計數(shù)為1或者大于1時,減去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]];
});
}
運行后報錯: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)存錯誤導致程序異常結(jié)束的概率很高.
-
代碼二:
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//生成Dispatch Semaphore ,計數(shù)初始值設定為1,保證可訪問NSMutableArray類對象的線程同時只能有1個
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的計數(shù)值達到或者大于1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //由于Dispatch Semaphore的計數(shù)值大于或等于1,Dispatch Semaphore的計數(shù)值減1,dispatch_semaphore_wait執(zhí)行返回. //此時Dispatch Semaphore的計數(shù)值等于0.由于可訪問NSMutableArray類對象的線程同時只能有1個,因此可安全的進行更新 [array addObject:[NSNumber numberWithInt:i]]; //排他控制處理結(jié)束,Dispatch Semaphore的計數(shù)值加1 dispatch_semaphore_signal(semaphore); });
}
NSLog(@"%lu",(unsigned long)array.count);
打印結(jié)果test01[2045:222660] 99999,更新成功
</pre>
也可以參考同步塊(synchronization block) 和NSLock的使用.
5.Dispatch_after
有時候會有這種情況,想在指定的時間后執(zhí)行處理.這時候可以考慮使用Dispatch_after.需要注意的是,Dispatch_after并不是在指定的時間后執(zhí)行處理,而只是在指定的時間追加處理到Dispatch queue.因為Mian Dispatch queue在主線程的RunLoop中執(zhí)行,所以在比如每隔1/60秒執(zhí)行的RunLoop中,Block最快3秒后執(zhí)行,最慢在3+1/60后執(zhí)行.雖然在有嚴格時間的要求下使用Dispatch_after會出問題,但在想大致延遲執(zhí)行處理時可以使用.
6.Dispatch_once
使用Dispatch_once來執(zhí)行只需要運行一次的線程安全代碼,即單例模式.常用的寫法如下:
<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ù)"NSOperationQueue",在某些情況下,使用NSOperationQueue比GCD更方便,我們也應該熟悉了解.
- 取消某個操作
在GCD中只負責往隊列中添加任務,無法取消.然而如果使用操作隊列.運行任務之前, 可以在NSOperation對象上調(diào)用cancel方法,該方法會設置對象內(nèi)的標志位,用以表示此任務不需執(zhí)行,不過,已啟動的任務無法取消. - 指定操作間的依賴關(guān)系
一個操作可以依賴其他多個操作.開發(fā)者能指定操作之間的依賴體系,是指定的操作必須在另外一個操 作順利執(zhí)行完畢后方可執(zhí)行. - 指定操作的優(yōu)先級
GCD也有優(yōu)先級,不過只能指定隊列的優(yōu)先級,而不能指定某個操作的優(yōu)先級. - 通知鍵值觀測機制監(jiān)控NSOperation對象的屬性
NSOperation對象有許多屬性都適合通過鍵值觀測機制(KVO)來監(jiān)聽,.比如可以通過isCancelled屬性來判斷任務是否已取消,也可以通過isFinished來判斷任務是否已完成.如果想在某個任務變更起狀態(tài)是得到通知,那么鍵值觀測很有用.
在多線程開發(fā)中,我們可以結(jié)合GCD和NSOperation,來更高效的實現(xiàn)多線程編程.