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