作者:@雷純鋒2011 授權本站轉載。
現如今移動設備也早已經進入了多核心 CPU 時代,並且隨著時間的推移,CPU 的核心數只會增加不會減少。而作為軟件開發者,我們需要做的就是盡可能地提高應用的並發性,來充分利用這些多核心 CPU 的性能。在 iOS 開發中,我們主要可以通過 Operation Queues、Dispatch Queues 和 Dispatch Sources 來提高應用的並發性。本文將主要介紹 Operation Queues 的相關知識,另外兩個屬於 Grand Central Dispatch(以下正文簡稱 GCD )的范疇,將會在後續的文章中進行介紹。
由於本文涉及的內容較多,所以建議讀者先提前了解一下本文的目錄結構,以便對本文有一個宏觀的認識:
基本概念
術語
串行 vs. 並發
同步 vs. 異步
隊列 vs. 線程
iOS 的並發編程模型
Operation Queues vs. Grand Central Dispatch (GCD)
關於 Operation 對象
並發 vs. 非並發 Operation
創建 NSInvocationOperation 對象
創建 NSBlockOperation 對象
自定義 Operation 對象
執行主任務
響應取消事件
配置並發執行的 Operation
維護 KVO 通知
定制 Operation 對象的執行行為
配置依賴關系
修改 Operation 在隊列中的優先級
修改 Operation 執行任務線程的優先級
設置 Completion Block
執行 Operation 對象
添加 Operation 到 Operation Queue 中
手動執行 Operation
取消 Operation
等待 Operation 執行完成
暫停和恢復 Operation Queue
總結
基本概念
在正式開始介紹 Operation Queues 的相關知識前,我想先介紹幾個在 iOS 並發編程中非常容易混淆的基本概念,以幫助讀者更好地理解本文。注,本文中的 Operation Queues 指的是 NSOperation 和 NSOperationQueue 的統稱。
術語
首先,我們先來了解一下在 iOS 並發編程中非常重要的三個術語,這是我們理解 iOS 並發編程的基礎:
進程(process),指的是一個正在運行中的可執行文件。每一個進程都擁有獨立的虛擬內存空間和系統資源,包括端口權限等,且至少包含一個主線程和任意數量的輔助線程。另外,當一個進程的主線程退出時,這個進程就結束了;
線程(thread),指的是一個獨立的代碼執行路徑,也就是說線程是代碼執行路徑的最小分支。在 iOS 中,線程的底層實現是基於 POSIX threads API 的,也就是我們常說的 pthreads ;
任務(task),指的是我們需要執行的工作,是一個抽象的概念,用通俗的話說,就是一段代碼。
串行 vs. 並發
從本質上來說,串行和並發的主要區別在於允許同時執行的任務數量。串行,指的是一次只能執行一個任務,必須等一個任務執行完成後才能執行下一個任務;並發,則指的是允許多個任務同時執行。
同步 vs. 異步
同樣的,同步和異步操作的主要區別在於是否等待操作執行完成,亦即是否阻塞當前線程。同步操作會等待操作執行完成後再繼續執行接下來的代碼,而異步操作則恰好相反,它會在調用後立即返回,不會等待操作的執行結果。
隊列 vs. 線程
有一些對 iOS 並發編程模型不太了解的同學可能會對隊列和線程產生混淆,不清楚它們之間的區別與聯系,因此,我覺得非常有必要在這裡簡單地介紹一下。在 iOS 中,有兩種不同類型的隊列,分別是串行隊列和並發隊列。正如我們上面所說的,串行隊列一次只能執行一個任務,而並發隊列則可以允許多個任務同時執行。iOS 系統就是使用這些隊列來進行任務調度的,它會根據調度任務的需要和系統當前的負載情況動態地創建和銷毀線程,而不需要我們手動地管理。
iOS 的並發編程模型
在其他許多語言中,為了提高應用的並發性,我們往往需要自行創建一個或多個額外的線程,並且手動地管理這些線程的生命周期,這本身就已經是一項非常具有挑戰性的任務了。此外,對於一個應用來說,最優的線程個數會隨著系統當前的負載和低層硬件的情況發生動態變化。因此,一個單獨的應用想要實現一套正確的多線程解決方案就變成了一件幾乎不可能完成的事情。而更糟糕的是,線程的同步機制大幅度地增加了應用的復雜性,並且還存在著不一定能夠提高應用性能的風險。
然而,值得慶幸的是,在 iOS 中,蘋果采用了一種比傳統的基於線程的系統更加異步的方式來執行並發任務。與直接創建線程的方式不同,我們只需定義好要調度的任務,然後讓系統幫我們去執行這些任務就可以了。我們可以完全不需要關心線程的創建與銷毀、以及多線程之間的同步等問題,蘋果已經在系統層面幫我們處理好了,並且比我們手動地管理這些線程要高效得多。
因此,我們應該要聽從蘋果的勸告,珍愛生命,遠離線程。不過話又說回來,盡管隊列是執行並發任務的首先方式,但是畢竟它們也不是什麼萬能的靈丹妙藥。所以,在以下三種場景下,我們還是應該直接使用線程的:
用線程以外的其他任何方式都不能實現我們的特定任務;
必須實時執行一個任務。因為雖然隊列會盡可能快地執行我們提交的任務,但是並不能保證實時性;
你需要對在後台執行的任務有更多的可預測行為。
Operation Queues vs. Grand Central Dispatch (GCD)
簡單來說,GCD 是蘋果基於 C 語言開發的,一個用於多核編程的解決方案,主要用於優化應用程序以支持多核處理器以及其他對稱多處理系統。而 Operation Queues 則是一個建立在 GCD 的基礎之上的,面向對象的解決方案。它使用起來比 GCD 更加靈活,功能也更加強大。下面簡單地介紹了 Operation Queues 和 GCD 各自的使用場景:
Operation Queues :相對 GCD 來說,使用 Operation Queues 會增加一點點額外的開銷,但是我們卻換來了非常強大的靈活性和功能,我們可以給 operation 之間添加依賴關系、取消一個正在執行的 operation 、暫停和恢復 operation queue 等;
GCD :則是一種更輕量級的,以 FIFO 的順序執行並發任務的方式,使用 GCD 時我們並不關心任務的調度情況,而讓系統幫我們自動處理。但是 GCD 的短板也是非常明顯的,比如我們想要給任務之間添加依賴關系、取消或者暫停一個正在執行的任務時就會變得非常棘手。
關於 Operation 對象
在 iOS 開發中,我們可以使用 NSOperation 類來封裝需要執行的任務,而一個 operation 對象(以下正文簡稱 operation )指的就是 NSOperation 類的一個具體實例。NSOperation 本身是一個抽象類,不能直接實例化,因此,如果我們想要使用它來執行具體任務的話,就必須創建自己的子類或者使用系統預定義的兩個子類,NSInvocationOperation 和 NSBlockOperation 。
NSInvocationOperation :我們可以通過一個 object 和 selector 非常方便地創建一個 NSInvocationOperation ,這是一種非常動態和靈活的方式。假設我們已經有了一個現成的方法,這個方法中的代碼正好就是我們需要執行的任務,那麼我們就可以在不修改任何現有代碼的情況下,通過方法所在的對象和這個現有方法直接創建一個 NSInvocationOperation 。
NSBlockOperation :我們可以使用 NSBlockOperation 來並發執行一個或多個 block ,只有當一個 NSBlockOperation 所關聯的所有 block 都執行完畢時,這個 NSBlockOperation 才算執行完成,有點類似於 dispatch_group 的概念。
另外,所有的 operation 都支持以下特性:
支持在 operation 之間建立依賴關系,只有當一個 operation 所依賴的所有 operation 都執行完成時,這個 operation 才能開始執行;
支持一個可選的 completion block ,這個 block 將會在 operation 的主任務執行完成時被調用;
支持通過 KVO 來觀察 operation 執行狀態的變化;
支持設置執行的優先級,從而影響 operation 之間的相對執行順序;
支持取消操作,可以允許我們停止正在執行的 operation 。
並發 vs. 非並發 Operation
通常來說,我們都是通過將 operation 添加到一個 operation queue 的方式來執行 operation 的,然而這並不是必須的。我們也可以直接通過調用 start 方法來執行一個 operation ,但是這種方式並不能保證 operation 是異步執行的。NSOperation 類的 isConcurrent 方法的返回值標識了一個 operation 相對於調用它的 start 方法的線程來說是否是異步執行的。在默認情況下,isConcurrent 方法的返回值是 NO ,也就是說會阻塞調用它的 start 方法的線程。
如果我們想要自定義一個並發執行的 operation ,那麼我們就必須要編寫一些額外的代碼來讓這個 operation 異步執行。比如,為這個 operation 創建新的線程、調用系統的異步方法或者其他任何方式來確保 start 方法在開始執行任務後立即返回。
在絕大多數情況下,我們都不需要去實現一個並發的 operation 。如果我們一直是通過將 operation 添加到 operation queue 的方式來執行 operation 的話,我們就完全沒有必要去實現一個並發的 operation 。因為,當我們將一個非並發的 operation 添加到 operation queue 後,operation queue 會自動為這個 operation 創建一個線程。因此,只有當我們需要手動地執行一個 operation ,又想讓它異步執行時,我們才有必要去實現一個並發的 operation 。
創建 NSInvocationOperation 對象
正如上面提到的,NSInvocationOperation 是 NSOperation 類的一個子類,當一個 NSInvocationOperation 開始執行時,它會調用我們指定的 object 的 selector 方法。通過使用 NSInvocationOperation 類,我們可以避免為每一個任務都創建一個自定義的子類,特別是當我們在修改一個已經存在的應用,並且這個應用中已經有了我們需要執行的任務所對應的 object 和 selector 時非常有用。
下面的示例代碼展示了如何通過 object 和 selector 創建一個 NSInvocationOperation 對象。說明,本文中的所有示例代碼都可以在這裡 OperationQueues 找到,每一個類都有與之對應的測試類,充當 client 的角色,建議你在看完一個小節的代碼時,運行一下相應的測試用例,觀察打印的結果,以加深理解。
@implementation OQCreateInvocationOperation - (NSInvocationOperation *)invocationOperationWithData:(id)data { return [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod1:) object:data]; } - (void)myTaskMethod1:(id)data { NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), data, [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @end
另外,我們在前面也提到了,NSInvocationOperation 類的使用可以非常的動態和靈活,其中比較顯著的一點就是我們可以根據上下文動態地調用 object 的不同 selector 。比如說,我們可以根據用戶的輸入動態地執行不同的 selector :
- (NSInvocationOperation *)invocationOperationWithData:(id)data userInput:(NSString *)userInput { NSInvocationOperation *invocationOperation = [self invocationOperationWithData:data]; if (userInput.length == 0) { invocationOperation.invocation.selector = @selector(myTaskMethod2:); } return invocationOperation; } - (void)myTaskMethod2:(id)data { NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), data, [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); }
創建 NSBlockOperation 對象
NSBlockOperation 是 NSOperation 類的另外一個系統預定義的子類,我們可以用它來封裝一個或多個 block 。我們知道 GCD 主要就是用來進行 block 調度的,那為什麼我們還需要 NSBlockOperation 類呢?一般來說,有以下兩個場景我們會優先使用 NSBlockOperation 類:
當我們在應用中已經使用了 Operation Queues 且不想創建 Dispatch Queues 時,NSBlockOperation 類可以為我們的應用提供一個面向對象的封裝;
我們需要用到 Dispatch Queues 不具備的功能時,比如需要設置 operation 之間的依賴關系、使用 KVO 觀察 operation 的狀態變化等。
下面的示例代碼展示了創建一個 NSBlockOperation 對象的基本方法:
@implementation OQCreateBlockOperation - (NSBlockOperation *)blockOperation { NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"Start executing block1, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block1"); }]; [blockOperation addExecutionBlock:^{ NSLog(@"Start executing block2, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block2"); }]; [blockOperation addExecutionBlock:^{ NSLog(@"Start executing block3, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block3"); }]; return blockOperation; } @end
自定義 Operation 對象
當系統預定義的兩個子類 NSInvocationOperation 和 NSBlockOperation 不能很好的滿足我們的需求時,我們可以自定義自己的 NSOperation 子類,添加我們想要的功能。目前,我們可以自定義非並發和並發兩種不同類型的 NSOperation 子類,而自定義一個前者要比後者簡單得多。
對於一個非並發的 operation ,我們需要做的就只是執行 main 方法中的任務以及能夠正常響應取消事件就可以了,其它的復雜工作比如依賴配置、KVO 通知等 NSOperation 類都已經幫我們處理好了。而對於一個並發的 operation ,我們還需要重寫 NSOperation 類中的一些現有方法。接下來,我們將會介紹如何自定義這兩種不同類型的 NSOperation 子類。
執行主任務
從最低限度上來說,每一個 operation 都應該至少實現以下兩個方法:
一個自定義的初始化方法;
main 方法。
我們需要用一個自定義的初始化方法來將創建的 operation 置於一個已知的狀態,並且重寫 main 方法來執行我們的任務。當然,我們也可以實現一些其他的額外方法,比如實現 NSCoding 協議來允許我們歸檔和解檔 operation 等。下面的示例代碼展示了如何自定義一個簡單的 operation :
@interface OQNonConcurrentOperation () @property (strong, nonatomic) id data; @end @implementation OQNonConcurrentOperation - (id)initWithData:(id)data { self = [super init]; if (self) { self.data = data; } return self; } /// 不支持取消操作 - (void)main { @try { NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), self.data, [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @catch(NSException *exception) { NSLog(@"Exception: %@", exception); } } @end
響應取消事件
當一個 operation 開始執行後,它會一直執行它的任務直到完成或被取消為止。我們可以在任意時間點取消一個 operation ,甚至是在它還未開始執行之前。為了讓我們自定義的 operation 能夠支持取消事件,我們需要在代碼中定期地檢查 isCancelled 方法的返回值,一旦檢查到這個方法返回 YES ,我們就需要立即停止執行接下來的任務。根據蘋果官方的說法,isCancelled 方法本身是足夠輕量的,所以就算是頻繁地調用它也不會給系統帶來太大的負擔。
The isCancelled method itself is very lightweight and can be called frequently without any significant performance penalty.
通常來說,當我們自定義一個 operation 類時,我們需要考慮在以下幾個關鍵點檢查 isCancelled 方法的返回值:
在真正開始執行任務之前;
至少在每次循環中檢查一次,而如果一次循環的時間本身就比較長的話,則需要檢查得更加頻繁;
在任何相對來說比較容易中止 operation 的地方。
看到這裡,我想你應該可以意識到一點,那就是盡管 operation 是支持取消操作的,但卻並不是立即取消的,而是在你調用了 operation 的 cancel 方法之後的下一個 isCancelled 的檢查點取消的。
/// 支持取消操作 - (void)main { @try { if (self.isCancelled) return; NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), self.data, [NSThread mainThread], [NSThread currentThread]); for (NSUInteger i = 0; i < 3; i++) { if (self.isCancelled) return; sleep(1); NSLog(@"Loop %@", @(i + 1)); } NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @catch(NSException *exception) { NSLog(@"Exception: %@", exception); } }
配置並發執行的 Operation
在默認情況下,operation 是同步執行的,也就是說在調用它的 start 方法的線程中執行它們的任務。而在 operation 和 operation queue 結合使用時,operation queue 可以為非並發的 operation 提供線程,因此,大部分的 operation 仍然可以異步執行。但是,如果你想要手動地執行一個 operation ,又想這個 operation 能夠異步執行的話,你需要做一些額外的配置來讓你的 operation 支持並發執行。下面列舉了一些你可能需要重寫的方法:
start :必須的,所有並發執行的 operation 都必須要重寫這個方法,替換掉 NSOperation 類中的默認實現。start 方法是一個 operation 的起點,我們可以在這裡配置任務執行的線程或者一些其它的執行環境。另外,需要特別注意的是,在我們重寫的 start 方法中一定不要調用父類的實現;
main :可選的,通常這個方法就是專門用來實現與該 operation 相關聯的任務的。盡管我們可以直接在 start 方法中執行我們的任務,但是用 main 方法來實現我們的任務可以使設置代碼和任務代碼得到分離,從而使 operation 的結構更清晰;
isExecuting 和 isFinished :必須的,並發執行的 operation 需要負責配置它們的執行環境,並且向外界客戶報告執行環境的狀態。因此,一個並發執行的 operation 必須要維護一些狀態信息,用來記錄它的任務是否正在執行,是否已經完成執行等。此外,當這兩個方法所代表的值發生變化時,我們需要生成相應的 KVO 通知,以便外界能夠觀察到這些狀態的變化;
isConcurrent :必須的,這個方法的返回值用來標識一個 operation 是否是並發的 operation ,我們需要重寫這個方法並返回 YES 。
下面我們將分三部分內容來介紹一下定義一個並發執行的 operation 所需的基本代碼,主體部分的代碼如下所示:
@implementation OQConcurrentOperation @synthesize executing = _executing; @synthesize finished = _finished; - (id)init { self = [super init]; if (self) { _executing = NO; _finished = NO; } return self; } - (BOOL)isConcurrent { return YES; } - (BOOL)isExecuting { return _executing; } - (BOOL)isFinished { return _finished; } @end
這一部分的代碼看上去比較簡單,但是卻需要我們用心地去理解它。首先,我們用 @synthesize 關鍵字手動合成了兩個實例變量 _executing 和 _finished ,然後分別在重寫的 isExecuting 和 isFinished 方法中返回了這兩個實例變量。另外,我們通過查看 NSOperation 類的頭文件可以發現,executing 和 finished 屬性都被聲明成了只讀的 readonly 。所以我們在 NSOperation 子類中就沒有辦法直接通過 setter 方法來自動觸發 KVO 通知,這也是為什麼我們需要在接下來的代碼中手動觸發 KVO 通知的原因。
接下來是 start 方法的代碼,在這個方法中,我們最需要關注的部分就是為 main 方法分離了一個新的線程,這是 operation 能夠並發執行的關鍵所在。此外,在真正開始執行任務前,我們通過檢查 isCancelled 方法的返回值來判斷 operation 是否已經被 cancel ,如果是就直接返回了。
- (void)start { if (self.isCancelled) { [self willChangeValueForKey:@"isFinished"]; _finished = YES; [self didChangeValueForKey:@"isFinished"]; return; } [self willChangeValueForKey:@"isExecuting"]; [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; _executing = YES; [self didChangeValueForKey:@"isExecuting"]; }
最後,是真正執行任務的 main 方法,值得注意的是在任務執行完畢後,我們需要手動觸動 isExecuting 和 isFinished 的 KVO 通知。
- (void)main { @try { NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]); sleep(3); [self willChangeValueForKey:@"isExecuting"]; _executing = NO; [self didChangeValueForKey:@"isExecuting"]; [self willChangeValueForKey:@"isFinished"]; _finished = YES; [self didChangeValueForKey:@"isFinished"]; NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @catch (NSException *exception) { NSLog(@"Exception: %@", exception); } }
注意,有一個非常重要的點需要引起我們的注意,那就是即使一個 operation 是被 cancel 掉了,我們仍然需要手動觸發 isFinished 的 KVO 通知。因為當一個 operation 依賴其他 operation 時,它會觀察所有其他 operation 的 isFinished 的值的變化,只有當它依賴的所有 operation 的 isFinished 的值為 YES 時,這個 operation 才能夠開始執行。因此,如果一個我們自定義的 operation 被取消了但卻沒有手動觸發 isFinished 的 KVO 通知的話,那麼所有依賴它的 operation 都不會執行。
維護 KVO 通知
NSOperation 類的以下 key paths 支持 KVO 通知,我們可以通過觀察這些 key paths 非常方便地監聽到一個 operation 內部狀態的變化:
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock
與重寫 main 方法不同的是,如果我們重寫了 start 方法或者對 NSOperation 類做了大量定制的話,我們需要保證自定義的 operation 在這些 key paths 上仍然支持 KVO 通知。比如,當我們重寫了 start 方法時,我們需要特別關注的是 isExecuting 和 isFinished 這兩個 key paths ,因為這兩個 key paths 最可能受重寫 start 方法的影響。
定制 Operation 對象的執行行為
我們可以在創建一個 operation 後,添加到 operation queue 前,對 operation 的一些執行行為進行定制。下面介紹的所有定制均適用於所有的 operation ,與是否是自定義的 NSOperation 子類或系統預定義的 NSOperation 子類無關。
配置依賴關系
通過配置依賴關系,我們可以讓不同的 operation 串行執行,正如我們前面提到的,一個 operation 只有在它依賴的所有 operation 都執行完成後才能開始執行。配置 operation 的依賴關系主要涉及到 NSOperation 類中的以下兩個方法:
- (void)addDependency:(NSOperation *)op; - (void)removeDependency:(NSOperation *)op;
顧名思義,第一個方法用於添加依賴,第二個方法則用於移除依賴。需要特別注意的是,用 addDependency: 方法添加的依賴關系是單向的,比如 [A addDependency:B]; ,表示 A 依賴 B,B 並不依賴 A 。
另外,這裡的依賴關系並不局限於相同 operation queue 中的 operation 之間。其實,從上面兩個配置依賴關系的方法是存在於 NSOperation 類中的,我們也可以看出來,operation 的依賴關系是它自己管理的,與它被添加到哪個 operation queue 無關。因此,我們完全可以給一些 operation 配置好依賴關系,然後將它們添加到不同的 operation queue 中。但是,有一點是需要我們特別注意的,就是不要在 operation 之間添加循環依賴,因為這樣會導致這些 operation 都不會被執行。
注意,我們應該在手動執行一個 operation 或將它添加到 operation queue 前配置好依賴關系,因為在之後添加的依賴關系可能會失效。
修改 Operation 在隊列中的優先級
對於被添加到 operation queue 中的 operation 來說,決定它們執行順序的第一要素是它們的 isReady 狀態,其次是它們在隊列中的優先級。operation 的 isReady 狀態取決於它的依賴關系,而在隊列中的優先級則是 operation 本身的屬性。默認情況下,所有新創建的 operation 的隊列優先級都是 normal 的,但是我們可以根據需要通過 setQueuePriority: 方法來提高或降低 operation 的隊列優先級。
需要注意的是,隊列優先級只應用於相同 operation queue 中的 operation 之間,不同 operation queue 中的 operation 不受此影響。另外,我們也需要清楚 operation 的隊列優先級和依賴關系之間的區別。operation 的隊列優先級只決定當前所有 isReady 狀態為 YES 的 operation 的執行順序。比如,在一個 operation queue 中,有一個高優先級和一個低優先級的 operation ,並且它們的 isReady 狀態都為 YES ,那麼高優先級的 operation 將會優先執行。而如果這個高優先級的 operation 的 isReady 狀態為 NO ,而低優先級的 operation 的 isReady 狀態為 YES 的話,那麼這個低優先級的 operation 反而會優先執行。
修改 Operation 執行任務線程的優先級
從 iOS 4.0 開始,我們可以修改 operation 的執行任務線程的優先級。雖然 iOS 系統中的線程策略是由 kernel 內核管理的,但是一般來說,高優先級的線程相對於低優先級的線程來說能夠得到更多的運行機會。我們可以給 operation 的線程優先級指定一個從 0.0 到 1.0 的浮點數值,0.0 表示最低的優先級,1.0 表示最高的優先級,默認值為 0.5 。
注意,我們只能夠在執行一個 operation 或將其添加到 operation queue 前,通過 operation 的 setThreadPriority: 方法來修改它的線程優先級。當 operation 開始執行時,NSOperation 類中默認的 start 方法會使用我們指定的值來修改當前線程的優先級。另外,我們指定的這個線程優先級只會影響 main 方法執行時所在線程的優先級。所有其它的代碼,包括 operation 的 completion block 所在的線程會一直以默認的線程優先級執行。因此,當我們自定義一個並發的 operation 類時,我們也需要在 start 方法中根據指定的值自行修改線程的優先級。
設置 Completion Block
從 iOS 4.0 開始,一個 operation 可以在它的主任務執行完成時回調一個 completion block 。我們可以用 completion block 來執行一些主任務之外的工作,比如,我們可以用它來通知一些客戶 operation 已經執行完畢,而並發的 operation 也可以用這個 block 來生成最終的 KVO 通知。如果需要設置一個 operation 的 completion block ,直接調用 NSOperation 類的 setCompletionBlock: 方法即可。
注意,當一個 operation 被取消時,它的 completion block 仍然會執行,所以我們需要在真正執行代碼前檢查一下 isCancelled 方法的返回值。另外,我們也沒有辦法保證 completion block 被回調時一定是在主線程,理論上它應該是與觸發 isFinished 的 KVO 通知所在的線程一致的,所以如果有必要的話我們可以在 completion block 中使用 GCD 來保證從主線程更新 UI 。
執行 Operation 對象
最終,我們需要執行 operation 來調度與其關聯的任務。目前,主要有兩種方式來執行一個 operation :
將 operation 添加到一個 operation queue 中,讓 operation queue 來幫我們自動執行;
直接調用 start 方法手動執行 operation 。
添加 Operation 到 Operation Queue 中
就目前來說,將 operation 添加到 operation queue 中是最簡單的執行 operation 的方式。另外,這裡的 operation queue 指的就是 NSOperationQueue 類的一個具體實例。就技術上而言,我們可以在應用中創建任意數量的 operation queue ,但是 operation queue 的數量越多並不意味著我們就能同時執行越多的 operation 。因為同時並發的 operation 數量是由系統決定的,系統會根據當前可用的核心數以及負載情況動態地調整最大的並發 operation 數量。創建一個 operation queue 非常簡單,跟創建其他普通對象沒有任何區別:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
創建好 operation queue 後,我們可以使用下面三個方法添加 operation 到 operation queue 中:
addOperation: ,添加一個 operation 到 operation queue 中;
addOperations:waitUntilFinished: ,添加一組 operation 到 operation queue 中;
addOperationWithBlock: ,直接添加一個 block 到 operation queue 中,而不用創建一個 NSBlockOperation 對象。
在大多數情況下,一個 operation 被添加到 operation queue 後不久就會執行,但是也有很多原因會使 operation queue 延遲執行入隊的 operation 。比如,我們前面提到了的,如果一個 operation 所依賴的其他 operation 還沒有執行完成時,這個 operation 就不能開始執行;再比如說 operation queue 被暫停執行或者已經達到了它最大可並發的 operation 數。下面的示例代碼展示了這三種方法的基本用法:
@implementation OQUseOperationQueue - (void)executeOperationUsingOperationQueue { NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init]; NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskMethod) object:nil]; [operationQueue addOperation:invocationOperation]; NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"Start executing blockOperation1, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing blockOperation1"); }]; NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"Start executing blockOperation2, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing blockOperation2"); }]; [operationQueue addOperations:@[ blockOperation1, blockOperation2 ] waitUntilFinished:NO]; [operationQueue addOperationWithBlock:^{ NSLog(@"Start executing block, mainThread: %@, currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block"); }]; [operationQueue waitUntilAllOperationsAreFinished]; } - (void)taskMethod { NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @end
注意,在將一個 operation 添加到 operation queue 後就不要再修改這個 operation 了。因為 operation 被添加到 operation queue 後隨時可能會執行,這個是由系統決定的,所以再修改它的依賴關系或者所包含的數據就很有可能會造成未知的影響。
盡管 NSOperationQueue 類是被設計成用來並發執行 operation 的,但是我們也可以強制一個 operation queue 一次只執行一個 operation 。我們可以通過 setMaxConcurrentoperationCount: 方法來設置一個 operation queue 最大可並發的 operation 數,因此將這個值設置成 1 就可以實現讓 operation queue 一次只執行一個 operation 的目的。但是需要注意的是,雖然這樣可以讓 operation queue 一次只執行一個 operation ,但是 operation 的執行順序還是一樣會受其他因素影響的,比如 operation 的 isReady 狀態、operation 的隊列優先級等。因此,一個串行的 operation queue 與一個串行的 dispatch queue 還是有本質區別的,因為 dispatch queue 的執行順序一直是 FIFO 的。如果 operation 的執行順序對我們來說非常重要,那麼我們就應該在將 operation 添加到 operation queue 之前就建立好它的依賴關系。
手動執行 Operation
盡管使用 operation queue 是執行一個 operation 最方便的方式,但是我們也可以不用 operation queue 而選擇手動地執行一個 operation 。從原理上來說,手動執行一個 operation 也是非常簡單的,只需要調用它的 start 方法就可以了。但是從嚴格意義上來說,在調用 start 方法真正開始執行一個 operation 前,我們應該要做一些防范性的判斷,比如檢查 operation 的 isReady 狀態是否為 YES ,這個取決於它所依賴的 operation 是否已經執行完成;又比如檢查 operation 的 isCancelled 狀態是否為 YES ,如果是,那麼我們就根本不需要再花費不必要的開銷去啟動它。
另外,我們應該一直通過 start 方法去手動執行一個 operation ,而不是 main 或其他的什麼方法。因為默認的 start 方法會在真正開始執行任務前為我們做一些安全性的檢查,比如檢查 operation 是否已取消等。另外,正如我們前面說的,在默認的 start 方法中會生成一些必要的 KVO 通知,比如 isExcuting 和 isFinished ,而這些 KVO 通知正是 operation 能夠正確處理好依賴關系的關鍵所在。
更進一步說,如果我們需要實現的是一個並發的 operation ,我們也應該在啟動 operation 前檢查一下它的 isConcurrent 狀態。如果它的 isConcurrent 狀態為 NO ,那麼我們就需要考慮一下是否可以在當前線程同步執行這個 operation ,或者是先為這個 operation 創建一個單獨的線程,以供它異步執行。
當然,如果你已經能夠確定一個 operation 的可執行狀態,那麼你大可不必做這些略顯啰嗦的防范性檢查,直接調用 start 方法執行這個 operation 即可。下面的示例代碼展示了手動執行一個 operation 的基本流程:
@implementation OQManualExecuteOperation - (BOOL)manualPerformOperation:(NSOperation *)operation { BOOL ranIt = NO; if (operation.isCancelled) { ranIt = YES; } else if (operation.isReady) { if (!operation.isConcurrent) { [operation start]; } else { [NSThread detachNewThreadSelector:@selector(start) toTarget:operation withObject:nil]; } ranIt = YES; } return ranIt; } @end
取消 Operation
從原則上來說,一旦一個 operation 被添加到 operation queue 後,這個 operation 的所有權就屬於這個 operation queue 了,並且不能夠被移除。唯一從 operation queue 中出隊一個 operation 的方式就是調用它的 cancel 方法取消這個 operation ,或者直接調用 operation queue 的 cancelAllOperations 方法取消這個 operation queue 中所有的 operation 。另外,我們前面也提到了,當一個 operation 被取消後,這個 operation 的 isFinished 狀態也會變成 YES ,這樣處理的好處就是所有依賴它的 operation 能夠接收到這個 KVO 通知,從而能夠清除這個依賴關系正常執行。
等待 Operation 執行完成
一般來說,為了讓我們的應用擁有最佳的性能,我們應該盡可能地異步執行所有的 operation ,從而讓我們的應用在執行這些異步 operation 的同時還能夠快速地響應用戶事件。當然,我們也可以調用 NSOperation 類的 waitUntilFinished 方法來阻塞當前線程,直到這個 operation 執行完成。雖然這種方式可以讓我們非常方便地處理 operation 的執行結果,但是卻給我們的應用引入了更多的串行,限制了應用的並發性,從而降低了我們應用的響應性。
注意,我們應該要堅決避免在主線程中去同步等待一個 operation 的執行結果,阻塞的方式只應該用在輔助線程或其他 operation 中。因為阻塞主線程會大大地降低我們應用的響應性,帶來非常差的用戶體驗。
除了等待一個單獨的 operation 執行完成外,我們也可以通過調用 NSOperationQueue 的 waitUntilAlloperationsAreFinished 方法來等待 operation queue 中的所有 operation 執行完成。有一點需要特別注意的是,當我們在等待一個 operation queue 中的所有 operation 執行完成時,其他的線程仍然可以向這個 operation queue 中添加 operation ,從而延長我們的等待時間。
暫停和恢復 Operation Queue
如果我們想要暫停和恢復執行 operation queue 中的 operation ,可以通過調用 operation queue 的 setSuspended: 方法來實現這個目的。不過需要注意的是,暫停執行 operation queue 並不能使正在執行的 operation 暫停執行,而只是簡單地暫停調度新的 operation 。另外,我們並不能單獨地暫停執行一個 operation ,除非直接 cancel 掉。
總結
看到這裡,我想你對 iOS 的並發編程模型已經有了一定的了解。正如文中所說的,我們應該盡可能地直接使用隊列而不是線程,讓系統去與線程打交道,而我們只需定義好要調度的任務就可以了。一般情況下,我們也完全不需要去自定義一個並發的 operation ,因為在與 operation queue 結合使用時,operation queue 會自動為非並發的 operation 創建一個線程。Operation Queues 是對 GCD 面向對象的封裝,它可以高度定制化,對依賴關系、隊列優先級和線程優先級等提供了很好的支持,是我們實現復雜任務調度時的不二之選。
參考鏈接
https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html
http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift
http://blog.xcodev.com/archives/operation-queue-intro/
版權聲明:我已將本文在微信公眾平台的發表權「獨家代理」給 iOS 開發(iOSDevTips)微信公眾號。