關於iOS多執行緒,這邊勉強可以看看(OC&Swift)

搜多執行緒谷歌給的第一張圖
iOS開發多執行緒總是繞不過的坎,看了很多前輩們優秀的文章,如: ofollow,noindex">關於iOS多執行緒,我說,你聽,沒準你就懂了! 、 談iOS多執行緒(NSThread、NSOperation、GCD)程式設計 、 iOS多執行緒:『GCD』詳盡總結 、 iOS多執行緒:『pthread、NSThread』詳盡總結 、 iOS多執行緒:『NSOperation、NSOperationQueue』詳盡總結 、 關於iOS多執行緒,你看我就夠了 等,但不自己整理一下,敲一下程式碼總是感覺不深刻 ,於是就有這篇文章,斷斷續續整理了好久。
示例我儘量把不同知識點程式碼獨立開,看著簡單一些,容易理解。示例程式碼我都用OC和Swift分別寫了,不過文中都是以OC說明的,英文如果用兩種語言一起,看起來可能比較亂,文章也會更加長(已經非常長了:joy::joy:)。
Swift寫法可能有比較大區別,也有些功能取消,但看一下程式碼應該能明白了。
文中的所有示例程式碼: andyRon/iOS-Multithreading
由於簡書沒有Markdown的目錄功能,我截了Typora的大綱,先看個大概:

1 簡介
1.1 一些概念
-
系統中正在執行的每一個應用程式都是一個 程序(Process) ,每個程序系統都會分配給它獨立的記憶體執行。也就是說,在iOS系統中中,每一個應用都是一個程序。
-
一個程序的所有任務都在 執行緒(Thread) 中進行,因此每個程序至少要有一個執行緒,也就是 主執行緒 。那多執行緒其實就是一個程序開啟多條執行緒,讓所有任務併發執行。
-
iOS App一旦執行,預設就會開啟一條執行緒。這條執行緒,通常稱作為“主執行緒”。在iOS應用中主執行緒的作用一般是:
重新整理UI;
處理UI事件,例如點選、滾動、拖拽。
-
如果主執行緒的操作太多、太耗時,就會造成App卡頓現象嚴重。所以,通常我們都會把耗時的操作放在子執行緒中進行,獲取到結果之後,回到主執行緒去重新整理UI。
-
多執行緒在一定意義上實現了程序內的資源共享,以及效率的提升。同時,在一定程度上相對獨立,它是程式執行流的最小單元,是程序中的一個實體,是執行程式最基本的單元,有自己棧和暫存器。
-
同步: 只能在當前執行緒按先後順序依次執行,不開啟新執行緒。
-
非同步: 可以在當前執行緒開啟多個新執行緒執行,可不按順序執行。
-
佇列: 裝載執行緒任務的隊形結構。
-
併發: 執行緒執行可以同時一起進行執行。
-
序列: 執行緒執行只能依次逐一先後有序的執行。
通過確保主執行緒自由響應使用者事件,併發可以很好地提高應用的響應性。通過將工作分配到多核,還能提高應用處理的效能。但是併發也帶來一定的額外開銷(排程),並且使程式碼更加複雜,更難編寫和除錯。
1.2 多執行緒概念補充
-
多執行緒的原理:
同一時間,CPU只能處理一條執行緒,也就是隻有一條執行緒在工作。所謂多執行緒併發(同時)執行,其實是CPU快速的在多執行緒之間排程(切換)。如果CPU排程執行緒的時間足夠快,就造成了多執行緒併發執行的假象。
-
在實際專案開發中並不是執行緒越多越好,如果開了大量的執行緒,會消耗大量的CPU資源,CPU會被累死,所以一般手機只開1~3個執行緒為宜,不超過5個。
-
多執行緒的優點:
能適當提高程式的執行效率
能適當提高資源的利用率,這個利用率表現在(CPU,記憶體的利用率)
-
多執行緒的缺點:
-
開啟執行緒需要佔用一定的記憶體空間(預設情況下,主執行緒佔用1M,子執行緒佔用512KB,如果開啟大量的執行緒,會佔用大量的記憶體空間,降低程式的效能)
-
執行緒越多,CPU在排程執行緒上的開銷就越大
-
執行緒越多,程式設計就越複雜,比如執行緒之間的通訊,多執行緒的資料共享,這些都需要程式的處理,增加了程式的複雜度。
-
-
在iOS開發中使用執行緒的注意事項:
- 別將比較耗時的操作放在主執行緒中
- 耗時操作會卡住主執行緒,嚴重影響UI的流暢度,給使用者一種“卡”的壞體驗
2 四種解決方案對比
- 目前iOS多執行緒有四種方法:pthread,NSThread,GCD, NSOperation,四種方案的簡單對比一下。

多執行緒的四種方案對比
由於pthread平常幾乎用不到,我暫時就不學了。
- 每個NSThread物件對應一個執行緒,真正最原始的執行緒,相對簡單,但是需要手動管理所有的執行緒活動,如生命週期、執行緒同步、睡眠等。
- 怎麼選擇 ?
簡單而安全的選擇NSOperation實現多執行緒即可。
處理大量併發資料,又追求效能效率的選擇GCD。
3 NSTread
生命週期還是需要程式設計師手動管理,所以這套方案也是偶爾用用。
3.1 NSThread三種執行緒開啟方式
- 動態開啟
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:(@"NSThread1")]; [thread1 start];
- 靜態開啟
// 建立好之後直接啟動 [NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:(@"NSTread2")];
- 隱式開啟
// 建立好之後也是直接啟動 [self performSelectorInBackground:@selector(doSomething3:) withObject:(@"NSTread3")];
3.2 NSThread拓展
- 獲取當前執行緒
NSThread *current = [NSThread currentThread];
- 獲取主執行緒
NSThread *main = [NSThread mainThread];
- 暫停當前執行緒一段時間
[NSThread sleepForTimeInterval:2];
- 暫停當前執行緒到某個時間
[NSThreadsleepUntilDate: date];
- 執行緒之間通訊
//在指定執行緒上執行操作 [self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES]; //在主執行緒上執行操作 [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES]; //在當前執行緒執行操作 [self performSelector:@selector(run) withObject:nil];
4. GCD

GCD為Grand Central Dispatch的縮寫。Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的較新的解決方法。它主要用於優化應用程式以支援多核處理器以及其他對稱多處理系統。
4.1 GCD的優點
- GCD 可用於多核的並行運算
- GCD 會自動利用更多的 CPU 核心(比如雙核、四核)
- GCD 會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)
- 程式設計師只需要告訴 GCD 想要執行什麼任務,不需要編寫任何執行緒管理程式碼
4.2 任務和佇列
-
任務: 表現上就是一段程式碼,OC就對應一個Block。 任務 有兩種執行方式, 是否會建立新的執行緒 , 會不會阻塞當前執行緒
- 同步執行 (sync):在當前執行緒執行任務,不會開闢新的執行緒。必須等到Block函式執行完畢後,dispatch函式才會返回。
- 非同步執行 (async):可以在新的執行緒中執行任務,但不一定會開闢新的執行緒。dispatch函式會立即返回, 然後Block在後臺非同步執行。
-
佇列:任務管理方式。分為 序列 和 並行 兩種方式,都是按照 FIFO(先進先出)原則依次觸發任務。
- 序列佇列 : 所有任務會在一條執行緒中執行(有可能是當前執行緒也有可能是新開闢的執行緒),並且一個任務執行完畢後,才開始執行下一個任務。(等待完成)
- 並行佇列 : 可以開啟多條執行緒並行執行任務(但不一定會開啟新的執行緒),並且當一個任務放到指定執行緒開始執行時,下一個任務就可以開始執行了。(等待發生)
兩者的區別: 執行順序不同,以及開啟執行緒數不同。
-
兩個特殊佇列:
- 主佇列 : 系統建立好的一個 序列佇列 ,它管理必須在主執行緒中執行的任務。
- 全域性佇列 :系統為我們建立好的一個 並行佇列 ,使用起來與我們自己建立的並行佇列無本質差別。
-
不同佇列建立獲取方式:
- (void)create { // dispatch_queue_create 第一個引數是佇列名字,一般用app的Bundle Identifier命名方式命名;第二個引數為NULL時表是序列佇列 //序列佇列 dispatch_queue_t serialQueue = dispatch_queue_create("q1.andyron.com", NULL); dispatch_queue_t serialQueue2 = dispatch_queue_create("q2.andyron.com", DISPATCH_QUEUE_SERIAL); //並行佇列 dispatch_queue_t concurrentQueue = dispatch_queue_create("q3.andyron.com", DISPATCH_QUEUE_CONCURRENT); //全域性並行佇列DISPATCH_QUEUE_PRIORITY_DEFAULT表示優先順序 dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //主佇列獲取 dispatch_queue_t mainQueue = dispatch_get_main_queue(); }
同步執行 | 非同步執行 | |
---|---|---|
序列佇列 | 當前執行緒,一個一個執行 | 其他執行緒,一個一個執行 |
並行佇列 | 當前執行緒,一個一個執行 | 開很多執行緒,一起執行 |
4.3 下面:point_down:以一個一個:chestnut:來學習GCD,幫助搞清上面的概念
- 例子一:執行緒死鎖(主佇列 + 同步執行)
- (void)case1 { NSLog(@"A=====%@", [NSThread currentThread]); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"B=====%@", [NSThread currentThread]); }); NSLog(@"C=====%@", [NSThread currentThread]); }
執行結果崩潰:

列印結果:
GCD(OC)[51511:6351422] A=====<NSThread: 0x600000064340>{number = 1, name = main}
解釋:預設就一個主佇列和一個主執行緒,因此 case1
函式這段任務就在主佇列中同步執行, dispatch_sync...
這段程式碼表示把B處任務加入主佇列中,並且同步執行,這就出問題,B處任務要等主佇列中同步執行之前的 case1
這段任務結束後執行,但B處任務在 case1
這段任務中, case1
又要等B處任務執行完才能繼續執行。 case1
任務要等B處完成才能繼續,但 case1
又排在B處前面,這就尷尬了, ̄□ ̄||,因此崩潰了:confounded:
- 例子二:主佇列 + 非同步執行
- (void)case2 { NSLog(@"A=====%@", [NSThread currentThread]); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"B=====%@", [NSThread currentThread]); }); NSLog(@"C=====%@", [NSThread currentThread]); }
結果:
GCD(OC)[52567:6465354] A=====<NSThread: 0x600000068d40>{number = 1, name = main} GCD(OC)[52567:6465354] C=====<NSThread: 0x600000068d40>{number = 1, name = main} GCD(OC)[52567:6465354] B=====<NSThread: 0x600000068d40>{number = 1, name = main}
解釋:任務都在主佇列(序列),而且只要一個主執行緒(name都是main),B處任務由於是非同步執行,等case2任務完成後執行。
- 例子三:序列佇列 + 同步執行
// 序列佇列 + 同步執行 - (void)case3 { dispatch_queue_t serialQueue = dispatch_queue_create("q2.andyron.com", DISPATCH_QUEUE_SERIAL); dispatch_sync(serialQueue, ^{ NSLog(@"1======%@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"2======%@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"3======%@", [NSThread currentThread]); }); NSLog(@"4======%@", [NSThread currentThread]); }
結果:
GCD(OC)[53734:6582112] 1======<NSThread: 0x604000261700>{number = 1, name = main} GCD(OC)[53734:6582112] 2======<NSThread: 0x604000261700>{number = 1, name = main} GCD(OC)[53734:6582112] 3======<NSThread: 0x604000261700>{number = 1, name = main} GCD(OC)[53734:6582112] 4======<NSThread: 0x604000261700>{number = 1, name = main}
解釋::chestnut:1中的主佇列也是序列佇列,但和這邊不同,這邊是新建了另一個序列佇列,不會出現衝突,並且都在主執行緒中執行,這也說明了同步執行不具備建立新執行緒的能力。
- 列子四:序列佇列 + 非同步執行
// 序列佇列 + 非同步執行 - (void)case4 { dispatch_queue_t serialQueue = dispatch_queue_create("q2.andyron.com", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); }); NSLog(@"4========%@",[NSThread currentThread]); }
列印結果:
GCD(OC)[53970:6604711] 4========<NSThread: 0x60000007c880>{number = 1, name = main} GCD(OC)[53970:6604933] 1========<NSThread: 0x600000460280>{number = 3, name = (null)} GCD(OC)[53970:6604933] 2========<NSThread: 0x600000460280>{number = 3, name = (null)} GCD(OC)[53970:6604933] 3========<NSThread: 0x600000460280>{number = 3, name = (null)}
解釋:現在列印了4,後列印了1,2,3,這是一部執行的結果,並且4在主執行緒,其它在子執行緒列印,這也說明了非同步執行可以建立新執行緒。
- 列子五:並行佇列 + 同步執行
/// 並行佇列 + 同步執行 - (void)case5 { dispatch_queue_t concurrentQueue = dispatch_queue_create("q3.andyron.com", DISPATCH_QUEUE_CONCURRENT); dispatch_sync(concurrentQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); //[self nslogCount:10000 number:1]; }); dispatch_sync(concurrentQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); //[self nslogCount:10000 number:2]; }); dispatch_sync(concurrentQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); //[self nslogCount:10000 number:3]; }); NSLog(@"4========%@",[NSThread currentThread]); }
列印結果:
GCD(OC)[54401:6646454] 1========<NSThread: 0x600000260e80>{number = 1, name = main} GCD(OC)[54401:6646454] 2========<NSThread: 0x600000260e80>{number = 1, name = main} GCD(OC)[54401:6646454] 3========<NSThread: 0x600000260e80>{number = 1, name = main} GCD(OC)[54401:6646454] 4========<NSThread: 0x600000260e80>{number = 1, name = main}
解釋:都在主執行緒執行,由於只有一個執行緒,結果看上去是順序執行。
- 列子六: 並行佇列 + 非同步執行
// 並行佇列 + 非同步執行 - (void)case6 { dispatch_queue_t concurrentQueue = dispatch_queue_create("q3.andyron.com", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^{ NSLog(@"1========%@",[NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"2========%@",[NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"3========%@",[NSThread currentThread]); }); NSLog(@"4========%@",[NSThread currentThread]); }
列印結果:
GCD(OC)[54687:6675227] 2========<NSThread: 0x600000466600>{number = 5, name = (null)} GCD(OC)[54687:6675036] 4========<NSThread: 0x604000078d00>{number = 1, name = main} GCD(OC)[54687:6675226] 1========<NSThread: 0x600000463fc0>{number = 4, name = (null)} GCD(OC)[54687:6675229] 3========<NSThread: 0x60400067d400>{number = 6, name = (null)}
解釋:除了列印4的是主線,其他又開啟了三個執行緒來執行三個任務,當天開啟幾個執行緒是有CPU自己決定的,任務的執行是隨機的。
4.4 GCD 重點
- 只要是序列佇列,肯定要等上一個任務執行完成,才能開始下一個任務。但是並行隊列當上一個任務開始執行後,下一個任務就可以開始執行。
- 同步+序列:未開闢新執行緒,序列執行任務;同步+並行:未開闢新執行緒,序列執行任務;非同步+序列:新開闢一條執行緒,序列執行任務;非同步+並行:開闢多條新執行緒,並行執行任務;在主執行緒中同步使用主佇列執行任務,會造成死鎖。
4.5 GCD其他相關方法
- 延遲執行方法:
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
指定時間後執行某個任務,dispatch_after
函式指定的時間是指多長後將任務加到某個佇列中,而不是具體執行時間,具體時間要看CPU執行時間了,可以看做是個大約延遲時間。
- (void)after { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2.0秒後非同步追加任務程式碼到主佇列,並開始執行 NSLog(@"after---%@",[NSThread currentThread]);// 列印當前執行緒 }); }
-
dispatch_once
:在生命週期內只執行一次。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLog(@"就一次%@",[NSThread currentThread]); }); }
不管點選多少次只有一次輸出。
GCD的內容很豐富,還有很多函式,2016和2017的WWDC都有專門講到GCD,想更近一步可以參考: Modernizing Grand Central Dispatch Usage , Concurrent Programming With GCD in Swift 3 。
5 NSOperation 和 NSOperationQueue
5.1 概念
-
NSOperation
( 操作 ) 和NSOperationQueue
( 操作佇列 ) 是蘋果對GCD的封裝 -
NSOperation
和NSOperationQueue
分別相當於 GCD 的 任務 和 佇列 -
NSOperation
只是一個抽象類,不能直接使用,使用其 2 個子類:NSInvocationOperation
和NSBlockOperation
。 -
NSOperation
的使用除了其現有的子類,還可以自定義子類。 -
操作佇列通過設定最大併發運算元(maxConcurrentOperationCount)來控制併發、序列。
-
NSOperationQueue 為我們提供了兩種不同型別的佇列:主佇列和自定義佇列。主佇列執行在主執行緒之上,而自定義佇列在後臺執行。
- NSOperation 需要配合 NSOperationQueue 來使用。否則,NSOperation 單獨使用時系統默認同步執行操作,配合 NSOperationQueue 我們能更好的實現非同步執行。
5. 2 NSOperation 實現多執行緒的步驟
- 建立操作:先將需要執行的操作封裝到一個 NSOperation 物件中。
- 建立佇列:建立 NSOperationQueue 物件。
- 將操作加入到佇列中:將 NSOperation 物件新增到NSOperationQueue 物件中。
- 之後,系統就會自動將 NSOperationQueue 中的 NSOperation 取出來,在新執行緒中執行操作。
5.3 使用NSOperation的子類NSInvocationOperation
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil]; [operation start];
在沒有使用 NSOperationQueue
時, NSInvocationOperation
會在當前執行緒(主執行緒或其他執行緒)內執行。
5.4 使用NSOperation的子類NSBlockOperation
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"%@", [NSThread currentThread]); }]; [operation start];
結果:
NSOperation(OC)[97406:5880178] 1---<NSThread: 0x60400006e700>{number = 1, name = main} NSOperation(OC)[97406:5880178] 1---<NSThread: 0x60400006e700>{number = 1, name = main}
在沒有使用 NSOperationQueue
時, NSBlockOperation
也會在當前執行緒(主執行緒或其他執行緒)內執行。
另外, NSBlockOperation
還提供了一個方法 addExecutionBlock:
,用來新增額外的操作:
// 1.建立 NSBlockOperation 物件 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒 }]; // 2.新增額外的操作 [op addExecutionBlock:^{ [NSThread sleepForTimeInterval:1]; // 模擬耗時操作 NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒 }]; [op addExecutionBlock:^{ [NSThread sleepForTimeInterval:1]; // 模擬耗時操作 NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒 }]; [op addExecutionBlock:^{ [NSThread sleepForTimeInterval:1]; // 模擬耗時操作 NSLog(@"4---%@", [NSThread currentThread]); // 列印當前執行緒 }]; [op addExecutionBlock:^{ [NSThread sleepForTimeInterval:1]; // 模擬耗時操作 NSLog(@"5---%@", [NSThread currentThread]); // 列印當前執行緒 }]; // 3.呼叫 start 方法開始執行操作 [op start];
某一次的執行結果:
NSOperation(OC)[97709:5888750] 1---<NSThread: 0x600000469740>{number = 3, name = (null)} NSOperation(OC)[97709:5888750] 5---<NSThread: 0x600000469740>{number = 3, name = (null)} NSOperation(OC)[97709:5888748] 3---<NSThread: 0x600000469e40>{number = 4, name = (null)} NSOperation(OC)[97709:5888498] 4---<NSThread: 0x60400007a600>{number = 1, name = main} NSOperation(OC)[97709:5888758] 2---<NSThread: 0x604000463480>{number = 5, name = (null)}
addExecutionBlock:
新增的操作和之前 blockOperationWithBlock
新增的操作是否在主執行緒或者是否開多執行緒,是由系統決定,它們的地位是相同的,所以每一次執行的結果可能不同。
5.5 NSOperation的自定義子類
除了上面兩個子類外,還可以通過重寫 main
方法來自定義子類。
#import "AROperation.h" @implementation AROperation - (void)main { if (!self.isCancelled) { for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; NSLog(@"自定義Operation---%@", [NSThread currentThread]); } } } @end
使用:
AROperation *op = [[AROperation alloc] init]; [op start];
5.6 NSOperationQueue
上面幾種情況都是沒有操作佇列,一般只在主執行緒執行。而使用操作佇列就可以實現多執行緒了。操作佇列分兩種:
主佇列: 凡是新增到主佇列中的操作,都會放到主執行緒中執行
自定義佇列: 操作自動放到子執行緒中執行,同時包含了:序列、併發功能。
5.6.1 建立佇列
// 主佇列獲取方法 NSOperationQueue *queue = [NSOperationQueue mainQueue]; // 自定義佇列建立方法 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
5.6.2 新增操作到佇列中
兩種不同的新增方法:
-
- (void)addOperation:(NSOperation *)op;
- (void)addOperationToQueue { // 1.建立佇列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.建立操作 // 使用 NSInvocationOperation 建立操作1 NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 使用 NSInvocationOperation 建立操作2 NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil]; // 使用 NSBlockOperation 建立操作3 NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [op3 addExecutionBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"4---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; // 3.使用 addOperation: 新增所有操作到佇列中 [queue addOperation:op1]; // [op1 start] [queue addOperation:op2]; // [op2 start] [queue addOperation:op3]; // [op3 start] } - (void) task1 { NSLog(@"1---%@", [NSThread currentThread]); } - (void) task2 { NSLog(@"2---%@", [NSThread currentThread]); }
某一次的執行結果:
NSOperation(OC)[7557:6218702] 2---<NSThread: 0x60400027f900>{number = 4, name = (null)} NSOperation(OC)[7557:6218699] 1---<NSThread: 0x600000461a00>{number = 3, name = (null)} NSOperation(OC)[7557:6218701] 4---<NSThread: 0x6040004606c0>{number = 5, name = (null)} NSOperation(OC)[7557:6218700] 3---<NSThread: 0x604000460d40>{number = 6, name = (null)} NSOperation(OC)[7557:6218700] 3---<NSThread: 0x604000460d40>{number = 6, name = (null)} NSOperation(OC)[7557:6218701] 4---<NSThread: 0x6040004606c0>{number = 5, name = (null)}
併發執行,執行次序不確定。
-
- (void)addOperationWithBlock:(void (^)(void))block;
不需要先建立操作,直接新增block
- (void)addOperationWithBlockToQueue { // 1.建立佇列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.使用 addOperationWithBlock: 新增操作到佇列中 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; }
某一次的執行結果:
NSOperation(OC)[7772:6229770] 2---<NSThread: 0x600000277640>{number = 5, name = (null)} NSOperation(OC)[7772:6229772] 1---<NSThread: 0x60400026bfc0>{number = 4, name = (null)} NSOperation(OC)[7772:6229780] 3---<NSThread: 0x6040002686c0>{number = 3, name = (null)} NSOperation(OC)[7772:6229770] 2---<NSThread: 0x600000277640>{number = 5, name = (null)} NSOperation(OC)[7772:6229780] 3---<NSThread: 0x6040002686c0>{number = 3, name = (null)} NSOperation(OC)[7772:6229772] 1---<NSThread: 0x60400026bfc0>{number = 4, name = (null)}
5.6.3 maxConcurrentOperationCount
NSOperationQueue
提供一個 maxConcurrentOperationCount
( 最大併發運算元 )屬性來控制序列還是併發。
maxConcurrentOperationCount
控制的不是併發執行緒的數量,而是一個佇列中同時能併發執行的最大運算元。而且一個操作也並非只能在一個執行緒中執行。
maxConcurrentOperationCount
預設情況下為-1,表示不進行限制。
- (void)setMaxConcurrentOperationCount { // 1.建立佇列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.設定最大併發運算元 queue.maxConcurrentOperationCount = 1; // 序列佇列 //queue.maxConcurrentOperationCount = 2; // 併發佇列 //queue.maxConcurrentOperationCount = 8; // 併發佇列 // 3.新增操作 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"4---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; }
當 最大併發運算元 為1時,也就是序列執行時,控制檯中返回的結果順序是固定的。而大於1時,也就是併發執行,每一次執行的順序就可能不同(控制檯中返回的額結果順序可能不同)。當然開啟的執行緒數量是有系統決定的。
5.7 NSOperation之間的依賴
並行時,各個的操作的執行順序是有系統決定,程式設計師不能直接控制。但是NSOperation提高了依賴,來解決這個問題。相關方法和屬性:
- (void)addDependency:(NSOperation *)op; - (void)removeDependency:(NSOperation *)op; @property (readonly, copy) NSArray<NSOperation *> *dependencies;
- (void)addDependency { // 1.建立佇列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.建立操作 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時操作 NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒 } }]; // 3.新增依賴 [op2 addDependency:op1]; // 讓op2 依賴於 op1,則先執行op1,再執行op2 // 4.新增操作到佇列中 [queue addOperation:op1]; [queue addOperation:op2]; }
執行結果是固定的:
NSOperation(OC)[8977:6302811] 1---<NSThread: 0x604000277f80>{number = 3, name = (null)} NSOperation(OC)[8977:6302811] 1---<NSThread: 0x604000277f80>{number = 3, name = (null)} NSOperation(OC)[8977:6302810] 2---<NSThread: 0x604000274240>{number = 4, name = (null)} NSOperation(OC)[8977:6302810] 2---<NSThread: 0x604000274240>{number = 4, name = (null)}
注意
:不能新增相互依賴,會死鎖,比如 A依賴B,B依賴A。
可以使用 removeDependency 來解除依賴關係。
可以在不同的佇列之間依賴,反正就是這個依賴是新增到任務身上的,和佇列沒關係。
5.8 NSOperation 常用屬性和方法
-
取消操作方法
- (void)cancel;
實質是標記 isCancelled 狀態。 -
判斷操作狀態方法
- (BOOL)isFinished; - (BOOL)isCancelled; - (BOOL)isExecuting; - (BOOL)isReady;
-
操作同步
- (void)waitUntilFinished; - (void)setCompletionBlock:(void (^)(void))block; - (void)addDependency:(NSOperation *)op; - (void)removeDependency:(NSOperation *)op; @property (readonly, copy) NSArray<NSOperation *> *dependencies;
5.9 NSOperationQueue 常用屬性和方法
- 取消/暫停/恢復操作
- (void)cancelAllOperations; - (BOOL)isSuspended; - (void)setSuspended:(BOOL)b;
- 操作同步
-
- (void)waitUntilAllOperationsAreFinished;
阻塞當前執行緒,直到佇列中的操作全部執行完畢。
- 新增/獲取操作
- (void)addOperationWithBlock:(void (^)(void))block; - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; - (NSArray *)operations; - (NSUInteger)operationCount;
- 獲取佇列
+ (id)currentQueue; + (id)mainQueue;
6 後記
雖然總結了很多,但還有很多內容沒有涉及和深入。
由於個人能力有限,時間緊湊(實際我已經花了很多時間:unamused: ♀️ ♀️),文中難免有錯誤,希望小夥伴批評指正。
示例程式碼: andyRon/iOS-Multithreading