現如今移動裝置也早已經進入了多核心 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
的角色,建議你在看完一個小節的程式碼時,執行一下相應的測試用例,觀察列印的結果,以加深理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", NSStringFromSelector(_cmd), data, [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing %@", NSStringFromSelector(_cmd)); } @end |
另外,我們在前面也提到了,NSInvocationOperation 類的使用可以非常的動態和靈活,其中比較顯著的一點就是我們可以根據上下文動態地呼叫 object
的不同 selector
。比如說,我們可以根據使用者的輸入動態地執行不同的 selector
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (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: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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 物件的基本方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@implementation OQCreateBlockOperation - (NSBlockOperation *)blockOperation { NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"Start executing block1, mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block1"); }]; [blockOperation addExecutionBlock:^{ NSLog(@"Start executing block2, mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block2"); }]; [blockOperation addExecutionBlock:^{ NSLog(@"Start executing block3, mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@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 { <a href='http://www.jobbole.com/members/xyz937134366'>@try</a> { NSLog(@"Start executing %@ with data: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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
的檢查點取消的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/// 支援取消操作 - (void)main { <a href='http://www.jobbole.com/members/xyz937134366'>@try</a> { if (self.isCancelled) return; NSLog(@"Start executing %@ with data: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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 所需的基本程式碼,主體部分的程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@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
,如果是就直接返回了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (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
通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)main { <a href='http://www.jobbole.com/members/xyz937134366'>@try</a> { NSLog(@"Start executing %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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)); } <a href='http://www.jobbole.com/members/wx895846013'>@catch</a> (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 類中的以下兩個方法:
1 2 |
- (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 非常簡單,跟建立其他普通物件沒有任何區別:
1 |
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 數。下面的示例程式碼展示了這三種方法的基本用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
@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: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing blockOperation1"); }]; NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"Start executing blockOperation2, mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing blockOperation2"); }]; [operationQueue addOperations:@[ blockOperation1, blockOperation2 ] waitUntilFinished:NO]; [operationQueue addOperationWithBlock:^{ NSLog(@"Start executing block, mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> currentThread: %@", [NSThread mainThread], [NSThread currentThread]); sleep(3); NSLog(@"Finish executing block"); }]; [operationQueue waitUntilAllOperationsAreFinished]; } - (void)taskMethod { NSLog(@"Start executing %<a href='http://www.jobbole.com/members/uz441800'>@,</a> mainThread: %<a href='http://www.jobbole.com/members/uz441800'>@,</a> 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 的基本流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@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
物件導向的封裝,它可以高度定製化,對依賴關係、佇列優先順序和執行緒優先順序等提供了很好的支援,是我們實現複雜任務排程時的不二之選。
參考連結
http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift