iOS 併發編程指南

iOS Concurrency Programming Guidenode

iOS 和 Mac OS 傳統的併發編程模型是線程,不過線程模型伸縮性不強,並且編寫正確的線程代碼也不容易。Mac OS 和 iOS 採起 asynchronous design approach 來解決併發的問題。程序員

引入的異步技術有兩個:編程

Grand Central Dispatch:系統管理線程,你不須要編寫線程代碼。只需定義想要執行的任務,而後添加到適當的dispatch queue。Grand Central Dispatch會負責建立線程和調度你的任務。系統直接提供線程管理,比應用實現更加高效。緩存

Operation Queue:Objective-C對象,相似於dispatch queue。你定義想要執行的任務,並添加任務到operation queue,後者負責調度和執行這些任務。和Grand Central Dispatch同樣,Operation Queue也管理了線程,更加高效。安全

Dispatch Queue服務器

基於C的執行自定義任務機制。dispatch queue按先進先出的順序,串行或併發地執行任務。serial dispaptch queue一次只能執行一個任務,直接當前任務完成纔開始出列並啓動下一個任務。而concurrent dispatch queue則儘量多地啓動任務併發執行。網絡

優勢:數據結構

直觀而簡單的編程接口併發

提供自動和總體的線程池管理app

提供彙編級調優的速度

更加高效地使用內存

不會trap內核under load

異步分派任務到dispatch queue不會致使queue死鎖

伸縮性強

serial dispatch queue比鎖和其它同步原語更加高效

Dispatch Sources

Dispatch Sources 是基於C的系統事件異步處理機制。一個Dispatch Source封裝了一個特定類型的系統事件,當事件發生時提交一個特定的block對象或函數到dispatch queue。你可使用Dispatch Sources監控如下類型的系統事件:

定時器

信號處理器

描述符相關的事件

進程相關的事件

Mach port事件

你觸發的自定義事件

Operation Queues

Operation Queues是Cocoa版本的併發dispatch queue,由 NSOperationQueue 類實現。dispatch queue老是按先進先出的順序執行任務,而 Operation Queues 在肯定任務執行順序時,還會考慮其它因素。最主要的一個因素是指定任務是否依賴於另外一個任務的完成。你在定義任務時配置依賴性,從而建立複雜的任務執行順序圖

提交到Operation Queues的任務必須是 NSOperation 對象,operation object封裝了你要執行的工做,以及所需的全部數據。因爲 NSOperation 是一個抽象基類,一般你須要定義自定義子類來執行任務。不過Foundation framework自帶了一些具體子類,你能夠建立並執行相關的任務。

Operation objects會產生key-value observing(KVO)通知,對於監控任務的進程很是有用。雖然operation queue老是併發地執行任務,你可使用依賴,在須要時確保順序執行

異步設計技術

經過確保主線程自由響應用戶事件,併發能夠很好地提升應用的響應性。經過將工做分配到多核,還能提升應用處理的性能。可是併發也帶來必定的額外開銷,而且使代碼更加複雜,更難編寫和調試代碼。

所以在應用設計階段,就應該考慮併發,設計應用須要執行的任務,及任務所需的數據結構。

Operation Queues

基於Objective-C,所以基於Cocoa的應用一般會使用Operation Queues

Operation Objects

operation object 是 NSOperation 類的實例,封裝了應用須要執行的任務,和執行任務所需的數據。NSOperation 自己是抽象基類,咱們必須實現子類。Foundation framework提供了兩個具體子類,你能夠直接使用:

 

描述
NSInvocationOperation 能夠直接使用的類,基於應用的一個對象和selector來建立operation object。若是你已經有現有的方法來執行須要的任務,就可使用這個類。
NSBlockOperation 能夠直接使用的類,用來併發地執行一個或多個block對象。operation object使用「組」的語義來執行多個block對象,全部相關的block都執行完成以後,operation object纔算完成。
NSOperation 基類,用來自定義子類operation object。繼承NSOperation能夠徹底控制operation object的實現,包括修改操做執行和狀態報告的方式。

全部operation objects都支持如下關鍵特性:

支持創建基於圖的operation objects依賴。能夠阻止某個operation運行,直到它依賴的全部operation都已經完成。

支持可選的completion block,在operation的主任務完成後調用。

支持應用使用KVO通知來監控operation的執行狀態。

支持operation優先級,從而影響相對的執行順序

支持取消,容許你停止正在執行的任務

併發 VS 非併發Operations

一般咱們經過將operation添加到operation queue中來執行該操做。可是咱們也能夠手動調用start方法來執行一個operation對象,這樣作不保證operation會併發執行。NSOperation類對象的 isConcurrent 方法告訴你這個operation相對於調用start方法的線程,是同步仍是異步執行的。isConcurrent 方法默認返回NO,表示operation與調用線程同步執行。

若是你須要實現併發operation,也就是相對調用線程異步執行的操做。你必須添加額外的代碼,來異步地啓動操做。例如生成一個線程、調用異步系統函數,以確保start方法啓動任務,並當即返回。

多數開發者歷來都不須要實現併發operation對象,咱們只須要將operations添加到operation queue。當你提交非併發operation到operation queue時,queue會建立線程來運行你的操做,所以也能達到異步執行的目的。只有你不但願使用operation queue來執行operation時,才須要定義併發operations。

建立一個 NSInvocationOperation 對象

若是已經現有一個方法,須要併發地執行,就能夠直接建立 NSInvocationOperation 對象,而不須要本身繼承 NSOperation。

  1. @implementation MyCustomClass 
  2. - (NSOperation*)taskWithData:(id)data { 
  3. NSInvocationOperation* theOp = [[[NSInvocationOperation alloc] initWithTarget:self 
  4. selector:@selector(myTaskMethod:) object:data] autorelease]; 
  5.  
  6. return theOp; 
  7.  
  8. // This is the method that does the actual work of the task. 
  9. - (void)myTaskMethod:(id)data { 
  10. // Perform the task. 
  11. @end 

建立一個 NSBlockOperation 對象

NSBlockOperation 對象用於封裝一個或多個block對象,通常建立時會添加至少一個block,而後再根據須要添加更多的block。當 NSBlockOperation 對象執行時,會把全部block提交到默認優先級的併發dispatch queue。而後 NSBlockOperation 對象等待全部block完成執行,最後標記本身已完成。所以可使用block operation來跟蹤一組執行中的block,有點相似於thread join等待多個線程的結果。區別在於block operation自己也運行在一個單獨的線程,應用的其它線程在等待block operation完成時能夠繼續工做。

  1. NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{ 
  2. NSLog(@"Beginning operation.\n"); 
  3. // Do some work. 
  4. }]; 

使用 addExecutionBlock: 能夠添加更多block到這個block operation對象。若是須要順序地執行block,你必須直接提交到所需的dispatch queue。

自定義Operation對象

若是block operation和invocation operation對象不符合應用的需求,你能夠直接繼承 NSOperation,並添加任何你想要的行爲。NSOperation 類提供通用的子類繼承點,並且實現了許多重要的基礎設施來處理依賴和KVO通知。繼承所需的工做量主要取決於你要實現非併發仍是併發的operation。

定義非併發operation要簡單許多,只須要執行主任務,並正確地響應取消事件;NSOperation 處理了其它全部事情。對於併發operation,你必須替換某些現有的基礎設施代碼。

執行主任務

每一個operation對象至少須要實現如下方法:

自定義initialization方法:初始化,將operation 對象設置爲已知狀態

自定義main方法:執行你的任務

你也能夠選擇性地實現如下方法:

main方法中須要調用的其它自定義方法

Accessor方法:設置和訪問operation對象的數據

dealloc方法:清理operation對象分配的全部內存

NSCoding 協議的方法:容許operation對象archive和unarchive

  1. @interface MyNonConcurrentOperation : NSOperation { 
  2. id myData; 
  3. -(id)initWithData:(id)data; 
  4. @end 
  5.  
  6. @implementation MyNonConcurrentOperation 
  7. - (id)initWithData:(id)data { 
  8. if (self = [super init]) 
  9. myData = [data retain]; 
  10. return self; 
  11.  
  12. - (void)dealloc { 
  13. [myData release]; 
  14. [super dealloc]; 
  15.  
  16. -(void)main { 
  17. @try { 
  18. NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 
  19. // Do some work on myData and report the results. 
  20. [pool release]; 
  21. @catch(...) { 
  22. // Do not rethrow exceptions. 
  23. @end 

響應取消事件

operation開始執行以後,會一直執行任務直到完成,或者顯式地取消操做。取消可能在任什麼時候候發生,甚至在operation執行以前。儘管 NSOperation 提供了一個方法,讓應用取消一個操做,可是識別出取消事件則是你的事情。若是operation直接終止,可能沒法回收全部已分配的內存或資源。所以operation對象須要檢測取消事件,並優雅地退出執行。

operation 對象按期地調用 isCancelled 方法,若是返回YES(表示已取消),則當即退出執行。無論是自定義 NSOperation 子類,仍是使用系統提供的兩個具體子類,都須要支持取消。isCancelled方法自己很是輕量,能夠頻繁地調用而不產生大的性能損失。如下地方可能須要調用isCancelled:

在執行任何實際的工做以前

在循環的每次迭代過程當中,若是每一個迭代相對較長可能須要調用屢次

代碼中相對比較容易停止操做的任何地方

  1. - (void)main { 
  2. @try { 
  3. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
  4. BOOL isDone = NO; 
  5.  
  6. while (![self isCancelled] && !isDone) { 
  7. // Do some work and set isDone to YES when finished 
  8. [pool release]; 
  9. @catch(...) { 
  10. // Do not rethrow exceptions. 

注意你的代碼還須要完成全部相關的資源清理工做

爲併發執行配置operations

Operation對象默認按同步方式執行,也就是在調用start方法的那個線程中直接執行。因爲operation queue爲非併發operation提供了線程支持,對應用來講,多數operations仍然是異步執行的。可是若是你但願手工執行operations,並且仍然但願可以異步執行操做,你就必須採起適當的措施,經過定義operation對象爲併發操做來實現。

方法 描述
start (必須)全部併發操做都必須覆蓋這個方法,以自定義的實現替換默認行爲。手動執行一個操做時,你會調用start方法。所以你對這個方法的實現是操做的起點,設置一個線程或其它執行環境,來執行你的任務。你的實如今任什麼時候候都絕對不能調用super。
main (可選)這個方法一般用來實現operation對象相關聯的任務。儘管你能夠在start方法中執行任務,使用main來實現任務可讓你的代碼更加清晰地分離設置和任務代碼
isExecuting
isFinished
(必須)併發操做負責設置本身的執行環境,並向外部client報告執行環境的狀態。所以併發操做必須維護某些狀態信息,以知道是否正在執行任務,是否已經完成任務。使用這兩個方法報告本身的狀態。
這兩個方法的實現必須可以在其它多個線程中同時調用。另外這些方法報告的狀態變化時,還須要爲相應的key path產生適當的KVO通知。
isConcurrent (必須)標識一個操做是否併發operation,覆蓋這個方法並返回YES

 

  1. @interface MyOperation : NSOperation { 
  2. BOOL        executing; 
  3. BOOL        finished; 
  4. - (void)completeOperation; 
  5. @end 
  6.  
  7. @implementation MyOperation 
  8. - (id)init { 
  9. self = [super init]; 
  10. if (self) { 
  11. executing = NO; 
  12. finished = NO; 
  13. return self; 
  14.  
  15. - (BOOL)isConcurrent { 
  16. return YES; 
  17.  
  18. - (BOOL)isExecuting { 
  19. return executing; 
  20.  
  21. - (BOOL)isFinished { 
  22. return finished; 
  23.  
  24. - (void)start { 
  25. // Always check for cancellation before launching the task. 
  26. if ([self isCancelled]) 
  27. // Must move the operation to the finished state if it is canceled. 
  28. [self willChangeValueForKey:@"isFinished"]; 
  29. finished = YES; 
  30. [self didChangeValueForKey:@"isFinished"]; 
  31. return; 
  32.  
  33. // If the operation is not canceled, begin executing the task. 
  34. [self willChangeValueForKey:@"isExecuting"]; 
  35. [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; 
  36. executing = YES; 
  37. [self didChangeValueForKey:@"isExecuting"]; 
  38.  
  39. - (void)main { 
  40. @try { 
  41. NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 
  42.  
  43. // Do the main work of the operation here. 
  44.  
  45. [self completeOperation]; 
  46. [pool release]; 
  47. @catch(...) { 
  48. // Do not rethrow exceptions. 
  49.  
  50. - (void)completeOperation { 
  51. [self willChangeValueForKey:@"isFinished"]; 
  52. [self willChangeValueForKey:@"isExecuting"]; 
  53.  
  54. executing = NO; 
  55. finished = YES; 
  56.  
  57. [self didChangeValueForKey:@"isExecuting"]; 
  58. [self didChangeValueForKey:@"isFinished"]; 
  59. @end 

即便操做被取消,你也應該通知KVO observers,你的操做已經完成。當某個operation對象依賴於另外一個operation對象的完成時,它會監測後者的isFinished key path。只有全部依賴的對象都報告已經完成,第一個operation對象纔會開始運行。若是你的operation對象沒有產生完成通知,就會阻止其它依賴於你的operation對象運行。

維護KVO依從

NSOperation類的key-value observing(KVO)依從於如下key paths:

isCancelled

isConcurrent

isExecuting

isFinished

isReady

dependencies

queuePriority

completionBlock

若是你覆蓋start方法,或者對NSOperation對象的其它自定義運行(覆蓋main除外),你必須確保自定義對象對這些key paths保留KVO依從。覆蓋start方法時,須要關注isExecuting和isFinished兩個key paths。

若是你但願實現依賴於其它東西(非operation對象),你能夠覆蓋isReady方法,並強制返回NO,直到你等待的依賴獲得知足。若是你須要保留默認的依賴管理系統,確保你調用了[super isReady]。當你的operation對象的準備就緒狀態發生改變時,生成一個isReady的key path的KVO通知。

除非你覆蓋了 addDependency: 或 removeDependency: 方法,不然你不須要關注dependencies key path

雖然你也能夠生成 NSOperation 的其它KVO通知,但一般你不須要這樣作。若是須要取消一個操做,你能夠直接調用現有的cancel方法。相似地,你也不多須要修改queue優先級信息。最後,除非你的operation對象能夠動態地改變併發狀態,你也不須要提供isConcurrent key path的KVO通知。

自定義一個Operation對象的執行行爲

對Operation對象的配置發生在建立對象以後,將其添加到queue以前。

配置operation之間的依賴關係

依賴關係能夠順序地執行相關的operation對象,依賴於其它操做,則必須等到該操做完成以後本身才能開始。你能夠建立一對一的依賴關係,也能夠建立多個對象之間的依賴圖。

使用 NSOperation 的 addDependency: 方法在兩個operation對象之間創建依賴關係。表示當前operation對象將依賴於參數指定的目標operation對象。依賴關係不侷限於相同queue中的operations對象,Operation對象會管理本身的依賴,所以徹底能夠在不一樣的queue之間的Operation對象建立依賴關係。

惟一的限制是不能建立環形依賴,這是程序員的錯誤,全部受影響的operations都沒法運行!

當一個operation對象依賴的全部其它對象都已經執行完成,該operation就變成準備執行狀態(若是你自定義了isReady方法,則由你的方法肯定是否準備好運行)。若是operation已經在一個queue中,queue就能夠在任什麼時候候執行這個operation。若是你須要手動執行該operation,就本身調用operation的start方法。

配置依賴必須在運行operation和添加operation到queue以前進行,以後添加的依賴關係可能不起做用。

依賴要求每一個operation對象在狀態發生變化時必須發出適當的KVO通知。若是你自定義了operation對象的行爲,就必須在自定義代碼中生成適當的KVO通知,以確保依賴可以正確地執行。

修改Operation的執行優先級

對於添加到queue的Operations,執行順序首先由已入隊列的operations是否準備好,而後再根據全部operations的相對優先級肯定。是否準備好由對象的依賴關係肯定,優先級等級則是operation對象自己的一個屬性。默認全部operation都擁有「普通」優先級,不過你能夠經過 setQueuePriority: 方法來提高或下降operation對象的優先級。

優先級只能應用於相同queue中的operations。若是應用有多個operation queue,每一個queue的優先級等級是互相獨立的。所以不一樣queue中的低優先級操做仍然可能比高優先級操做更早執行。

優先級不能替代依賴關係,優先級只是queue對已經準備好的operations肯定執行順序。先知足依賴關係,而後再根據優先級從全部準備好的操做中選擇優先級最高的那個執行。

修改底層線程的優先級

Mac OS X 10.6以後,咱們能夠配置operation底層線程的執行優先級,線程直接由內核管理,一般優先級高的線程會給予更多的執行機會。對於operation對象,你指定線程優先級爲0.0到1.0之間的某個數值,0.0表示最低優先級,1.0表示最高優先級。默認線程優先級爲0.5

要設置operation的線程優先級,你必須在將operation添加到queue以前,調用 setThreadPriority: 方法進行設置。當queue執行該operation時,默認的start方法會使用你指定的值來修改當前線程的優先級。不過新的線程優先級只在operation的main方法範圍內有效。其它全部代碼仍然(包括completion block)運行在默認線程優先級。

若是你建立了併發operation,並覆蓋了start方法,你必須本身配置線程優先級。

設置一個completion block

在Mac OS X 10.6以後,operation能夠在主任務完成以後執行一個completion block。你可使用這個completion block來執行任何不屬於主任務的工做。例如你可使用這個block來通知相關的client,操做已經執行完成。而併發operation對象則可使用這個block來產生最終的KVO通知。

調用 NSOperation 的 setCompletionBlock: 方法來設置一個completion block,你傳遞的block應該沒有參數和返回值。

實現Operation對象的技巧

Operation對象的內存管理

operation對象須要良好的內存管理策略

建立你本身的Autorelease Pool

operation是Objective-C對象,你在實現任務的代碼中應該建立一個autorelease pool,這樣能夠保護那些autorelease對象獲得儘快地釋放。雖然你的自定義代碼執行時可能已經有了一個pool,但你不能依賴於這個行爲,老是應該本身建立一個。

擁有本身的autorelease pool還能更加靈活地管理operation的內存。若是operation建立大量的臨時對象,則能夠考慮建立額外的pool,來清理再也不使用的臨時對象。在iOS*****別須要注意,應早晚地清理再也不使用的臨時對象,避免內存警告。

  1. - (void)main { 
  2. @try { 
  3. NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 
  4.  
  5. // Do the main work of the operation here. 
  6.  
  7. [pool release]; 
  8. @catch(...) { 
  9. // Do not rethrow exceptions. 

避免Per-Thread存儲

雖然多數operation都在線程中執行,但對於非併發operation,一般由operation queue提供線程,這時候queue擁有該線程,而你的應用不該該去動這個線程。特別是不要關聯任何數據到不是你建立和擁有的線程。這些線程由queue管理,根據系統和應用的需求建立或銷燬。所以使用Per-Thread storage在operations之間傳遞數據是不可靠的,並且頗有可能會失敗。

對於operation對象,你徹底沒有理由使用Per-Thread Storage,應該在建立對象的時候就給它須要的全部數據。全部輸入和輸出數據都應該存儲在operation對象中,最後再整合到你的應用,或者最終釋放掉。

根據須要保留Operation對象的引用

因爲operation對象異步執行,你不能建立完之後就徹底無論。它們也是對象,須要你來分配和釋放它們管理的任何資源,特別是若是你須要在operation對象完成後獲取其中的數據。

因爲queue老是盡最大可能快速地調度和執行operation,在你添加operation到queue時,可能當即就開始運行,當你稍後向queue請求operation對象的狀態時,有可能queue已經執行完了相應的operation並從queue中刪除了這個對象。所以你老是應該本身擁有operation對象的引用。

處理錯誤和異常

operation本質上是應用中獨立的實體,所以須要本身負責處理全部的錯誤和異常。NSOperation默認的start方法並無捕獲異常。因此你本身的代碼老是應該捕獲並抑制異常。你還應該檢查錯誤代碼並適當地通知應用。若是你覆蓋了start方法,你也必須捕獲全部異常,阻止它離開底層線程的範圍。

你須要準備好處理如下錯誤或異常:

檢查並處理UNIX errno風格的錯誤代碼

檢查方法或函數顯式返回的錯誤代碼

捕獲你的代碼或系統frameworks拋出的異常

捕獲NSOperation類本身拋出的異常,在如下狀況NSOperation會拋出異常:

operation沒有準備好,可是調用了start方法

operation正在執行或已經完成(可能被取消),再次調用了start方法。

當你添加completion block到正在執行或已經完成的operation

當你試圖獲取已經取消 NSInvocationOperation 對象的結果

爲Operation對象肯定一個適當的範圍

和任何對象同樣,NSOperation對象也會消耗內存,執行時也會帶來開銷。所以若是operation對象只作不多的工做,可是卻建立成千上萬個小的operation對象,你就會發現更多的時間花在了調度operations而不是執行它們。

要高效地使用Operations,關鍵是在Operation執行的工做量和保持計算機繁忙之間,找到最佳的平衡。確保每一個Operation都有必定的工做量能夠執行。例如100個operations執行100次相同任務,能夠考慮換成10個operations,每一個執行10次。

你一樣要避免向一個queue中添加過多的operations,或者持續快速地向queue中添加operation,超過queue所能處理的能力。這裏能夠考慮分批建立operations對象,在一批對象執行完以後,使用completion block告訴應用建立下一批operations對象。

執行Operations

應用須要執行Operations來處理相關的工做,你有幾種方法來執行Operations對象。

添加Operations到Operation Queue

執行Operations最簡單的方法是添加到operation queue,後者是 NSOperationQueue 對象。應用負責建立和維護本身使用的全部 NSOperationQueue 對象。

  1. NSOperationQueue* aQueue = [[NSOperationQueue alloc] init]; 

調用 addOperation: 方法添加一個operation到queue,Mac OS X 10.6以後可使用 addOperations:waitUntilFinished: 方法一次添加一組operations,或者也能夠直接使用 addOperationWithBlock: 方法添加 block 對象到queue。

  1. [aQueue addOperation:anOp]; // Add a single operation 
  2. [aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations 
  3. [aQueue addOperationWithBlock:^{ 
  4. /* Do something. */ 
  5. }]; 

Operations添加到queue後,一般短期內就會獲得運行。可是若是存在依賴,或者Operations掛起等緣由,也可能須要等待。

注意Operations添加到queue以後,絕對不要再修改Operations對象。由於Operations對象可能會在任什麼時候候運行,所以改變依賴或數據會產生不利的影響。你只能經過 NSOperation 的方法來查看操做的狀態,是否正在運行、等待運行、已經完成等。

雖然 NSOperationQueue 類設計用於併發執行Operations,你也能夠強制單個queue一次只能執行一個Operation。setMaxConcurrentOperationCount: 方法能夠配置operation queue的最大併發操做數量。設爲1就表示queue每次只能執行一個操做。不過operation執行的順序仍然依賴於其它因素,像操做是否準備好和優先級等。所以串行化的operation queue並不等同於Grand Central Dispatch中的串行dispatch queue。

手動執行Operations

手動執行Operation,要求Operation已經準備好,isReady返回YES,此時你才能調用start方法來執行它。isReady方法與Operations依賴是結合在一塊兒的。

調用start而不是main來手動執行Operation,由於start在執行你的自定義代碼以前,會首先執行一些安全檢查。並且start還會產生KVO通知,以正確地支持Operations的依賴機制。start還能處理Operations已經被取消的狀況,此時會拋出一個異常。

手動執行Operation對象以前,還須要調用 isConcurrent 方法,若是返回NO,你的代碼能夠決定在當前線程同步執行這個Operation,或者建立一個獨立的線程以異步執行。

下面方法演示了手動執行Operation,若是這個方法返回NO,表示不能執行,你須要設置一個定時器,稍後再次調用本方法,直到這個方法返回YES,表示已經執行Operation。

  1. - (BOOL)performOperation:(NSOperation*)anOp 
  2. BOOL        ranIt = NO; 
  3.  
  4. if ([anOp isReady] && ![anOp isCancelled]) 
  5. if (![anOp isConcurrent]) 
  6. [anOp start]; 
  7. else 
  8. [NSThread detachNewThreadSelector:@selector(start) 
  9. toTarget:anOp withObject:nil]; 
  10. ranIt = YES; 
  11. else if ([anOp isCancelled]) 
  12. // If it was canceled before it was started, 
  13. //  move the operation to the finished state. 
  14. [self willChangeValueForKey:@"isFinished"]; 
  15. [self willChangeValueForKey:@"isExecuting"]; 
  16. executing = NO; 
  17. finished = YES; 
  18. [self didChangeValueForKey:@"isExecuting"]; 
  19. [self didChangeValueForKey:@"isFinished"]; 
  20.  
  21. // Set ranIt to YES to prevent the operation from 
  22. // being passed to this method again in the future. 
  23. ranIt = YES; 
  24. return ranIt; 

取消Operations

一旦添加到operation queue,queue就擁有了這個對象而且不能被刪除,惟一能作的事情是取消。你能夠調用Operation對象的cancel方法取消單個操做,也能夠調用operation queue的 cancelAllOperations 方法取消當前queue中的全部操做。

只有你肯定再也不須要Operations對象時,才應該取消它。發出取消命令會將Operations對象設置爲"Canceled"狀態,會阻止它被執行。因爲取消也被認爲是完成,依賴於它的其它Operations對象會收到適當的KVO通知,並清除依賴狀態,而後獲得執行。

所以常見的作法是當發生重大事件時,一次性取消queue中的全部操做,例如應用退出或用戶請求取消操做。

等待Operations完成

爲了最佳的性能,你應該儘可能設計你的應用盡量地異步操做,讓應用在操做正在執行時能夠去處理其它事情。

若是建立operation的代碼須要處理operation完成後的結果,可使用 NSOperation 的 waitUntilFinished 方法等待operation完成。一般咱們應該避免編寫這樣的代碼,阻塞當前線程多是一種簡便的解決方案,可是它引入了更多的串行代碼,限制了整個應用的併發性,同時也下降了用戶體驗。

絕對不要在應用主線程中等待一個Operation,只能在第二或次要線程中等待。阻止主線程將致使應用沒法響應用戶事件,應用也將表現爲無響應。

除了等待單個Operation完成,你也能夠同時等待一個queue中的全部操做,使用 NSOperationQueue 的 waitUntilAllOperationsAreFinished 方法。注意在等待一個queue時,應用的其它線程仍然能夠往queue中添加Operation,所以可能加長你線程的等待時間。

掛起和繼續Queue

若是你想臨時掛起Operations的執行,可使用 setSuspended: 方法暫停相應的queue。不過掛起一個queue不會致使正在執行的Operation在任務中途暫停,只是簡單地阻止調度新Operation執行。你能夠在響應用戶請求時,掛起一個queue,來暫停等待中的任務。稍後根據用戶的請求,能夠再次調用 setSuspended: 方法繼續Queue中操做的執行。

 

Dispatch Queues

dispatch queues是執行任務的強大工具,容許你同步或異步地執行任意代碼block。原先使用單獨線程執行的全部任務均可以替換爲使用dispatch queues。而dispatch queues最大的優勢在於使用簡單,並且更加高效。

dispatch queues任務的概念就是應用須要執行的一些工做,如計算、建立或修改數據結構、處理數據等等。咱們使用函數或block對象來定義任務,並添加到dispatch queue。

dispatch queue是相似於對象的結構體,管理你提交給它的任務,並且都是先進先出的數據結構。所以queue中的任務老是以添加的順序開始執行。Grand Central Disaptch提供了幾種dispatch queues,不過你也本身建立。

類型 描述
串行 也稱爲private dispatch queue,每次只執行一個任務,按任務添加順序執行。當前正在執行的任務在獨立的線程中運行(不一樣任務的線程可能不一樣),dispatch queue管理了這些線程。一般串行queue主要用於對特定資源的同步訪問。
你能夠建立任意數量的串行queues,雖然每一個queue自己每次只能執行一個任務,可是各個queue之間是併發執行的。
併發 也稱爲global dispatch queue,能夠併發執行一個或多個任務,可是任務仍然是以添加到queue的順序啓動。每一個任務運行於獨立的線程中,dispatch queue管理全部線程。同時運行的任務數量隨時都會變化,並且依賴於系統條件。
你不能建立併發dispatch queues。相反應用只能使用三個已經定義好的全局併發queues。
Main dispatch queue 全局可用的串行queue,在應用主線程中執行任務。這個queue與應用的 run loop 交叉執行。因爲它運行在應用的主線程,main queue一般用於應用的關鍵同步點。
雖然你不須要建立main dispatch queue,但你必須確保應用適當地回收

應用使用dispatch queue,相比線程有不少優勢,最直接的優勢是簡單,不用編寫線程建立和管理的代碼,讓你集中精力編寫實際工做的代碼。另外系統管理線程更加高效,而且能夠動態調控全部線程。

dispatch queue比線程具備更強的可預測性,例如兩個線程訪問共享資源,你可能沒法控制哪一個線程前後訪問;可是把兩個任務添加到串行queue,則能夠確保兩個任務對共享資源的訪問順序。同時基於queue的同步也比基於鎖的線程同步機制更加高效。

應用有效地使用dispatch queue,要求儘量地設計自包含、能夠異步執行的任務。

dispatch queues的幾個關鍵點:

dispatch queues相對其它dispatch queues併發地執行任務,串行化任務只能在同一個dispatch queue中實現。

系統決定了同時可以執行的任務數量,應用在100個不一樣的queues中啓動100個任務,並不表示100個任務所有都在併發地執行(除非系統擁有100或更多個核)

系統在選擇執行哪一個任務時,會考慮queue的優先級。

queue中的任務必須在任什麼時候候都準備好運行,注意這點和Operation對象不一樣。

private dispatch queue是引用計數的對象。你的代碼中須要retain這些queue,另外dispatch source也可能添加到一個queue,從而增長retain的計數。所以你必須確保全部dispatch source都被取消,並且適當地調用release。

Queue相關的技術

除了dispatch queue,Grand Central Disaptch還提供幾個相關的技術,使用queue來幫助你管理代碼。

技術 描述
Dispatch group 用於監控一組block對象完成(你能夠同步或異步地監控block)。Group提供了一個很是有用的同步機制,你的代碼能夠等待其它任務的完成
Dispatch semaphore 相似於傳統的semaphore(信號量),可是更加高效。只有當調用線程因爲信號量不可用,須要阻塞時,Dispatch semaphore纔會去調用內核。若是信號量可用,就不會與內核進行交互。使用信號量能夠實現對有限資源的訪問控制
Dispatch source Dispatch source在特定類型的系統事件發生時,會產生通知。你可使用dispatch source來監控各類事件,如:進程通知、信號、描述符事件、等等。當事件發生時,dispatch source異步地提交你的任務到指定的dispatch queue,來進行處理。
使用Block實現任務

Block能夠很是容易地定義「自包含」的工做單元,儘管看上去很是相似於函數指針,block實際上由底層數據結構來表示,由編譯器負責建立和管理。編譯器對你的代碼(和全部相關的數據)進行打包,封裝爲能夠存在於堆中的格式,並在你的應用各個地方傳遞。

Block最關鍵的優勢可以使用own lexical scope以外的變量,在函數或方法內部定義一個block時,block能夠直接讀取父scope中的變量。block訪問的變量所有被拷貝到block在堆中的數據結構,這樣block就能在稍後自由地訪問這些變量。當block被添加到dispatch queue中時,這些變量一般是隻讀格式的。不過同步執行的Block對象,可使用那些定義爲__block的變量,對這些變量的修改會影響到調用scope。

Block的簡單用法:

  1. int x = 123; 
  2. int y = 456; 
  3.   
  4. // Block declaration and assignment 
  5. void (^aBlock)(int) = ^(int z) { 
  6.     printf("%d %d %d\n", x, y, z); 
  7. }; 
  8.   
  9. // Execute the block 
  10. aBlock(789);   // prints: 123 456 789 

 

設計Block時需考慮如下關鍵指導方針:

對於使用dispatch queue的異步Block,能夠在Block中安全地捕獲和使用父函數或方法中的scalar變量。可是Block不該該去捕獲大型結構體或其它基於指針的變量,這些變量由Block的調用上下文分配和刪除。在你的Block被執行時,這些指針引用的內存可能已經不存在。固然,你本身顯式地分配內存(或對象),而後讓Block擁有這些內存的全部權,是安全可行的。

Dispatch queue對添加的Block會進行復制,在完成執行後自動釋放。換句話說,你不須要在添加Block到Queue時顯式地複製

儘管Queue執行小任務比原始線程更加高效,仍然存在建立Block和在Queue中執行的開銷。若是Block作的事情太少,可能直接執行比dispatch到queue更加有效。使用性能工具來確認Block的工做是否太少

絕對不要針對底層線程緩存數據,而後指望在不一樣Block中可以訪問這些數據。若是相同queue中的任務須要共享數據,應該使用dispatch queue的context指針來存儲這些數據。

若是Block建立了大量Objective-C對象,考慮建立本身的autorelease pool,來處理這些對象的內存管理。雖然dispatch queue也有本身的autorelease pool,但不保證在何時會回收這些pool。

建立和管理Dispatch Queue

得到全局併發Dispatch Queue

併發dispatch queue能夠同時並行地執行多個任務,不過併發queue仍然按先進先出的順序來啓動任務,併發queue會在以前任務完成以前就出列下一個任務並啓動執行。併發queue同時執行的任務數量會根據應用和系統動態變化,各類因素包括:可用核數量、其它進程正在執行的工做數量、其它串行dispatch queue中優先任務的數量等。

系統給每一個應用提供三個併發dispatch queue,全部應用全局共享,三個queue的區別是優先級。你不須要顯式地建立這些queue,使用 dispatch_get_global_queue 函數來獲取這三個queue:

  1. dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 

除了默認優先級的併發queue,你還能夠得到高和低優先級的兩個,分別使用 DISPATCH_QUEUE_PRIORITY_HIGH 和 DISPATCH_QUEUE_PRIORITY_LOW 常量來調用上面函數。

雖然dispatch queue是引用計數的對象,但你不須要retain和release全局併發queue。由於這些queue對應用是全局的,retain和release調用會被忽略。

你也不須要存儲這三個queue的引用,每次都直接調用 dispatch_get_global_queue 得到queue就好了。

建立串行Dispatch Queue

應用的任務須要按特定順序執行時,就須要使用串行Dispatch Queue,串行queue每次只能執行一個任務。你可使用串行queue來替代鎖,保護共享資源或可變的數據結構。和鎖不同的是,串行queue確保任務按可預測的順序執行。並且只要你異步地提交任務到串行queue,就永遠不會產生死鎖。

你必須顯式地建立和管理全部你使用的串行queue,應用能夠建立任意數量的串行queue,但不要爲了同時執行更多任務而建立更多的串行queue。若是你須要併發地執行大量任務,應該把任務提交到全局併發Queue。

建立串行queue時,你須要明確本身的目的,如保護共享資源,或同步應用的某些關鍵行爲。

dispatch_queue_create 函數建立串行queue,兩個參數分別是queue名和一組queue屬性。調試器和性能工具會顯示queue的名字,便於你跟蹤任務的執行。

  1. dispatch_queue_t queue; 
  2. queue = dispatch_queue_create("com.example.MyQueue", NULL); 

運行時得到公共Queue

Grand Central Disaptch提供函數,讓應用訪問幾個公共dispatch queue:

使用 dispatch_get_current_queue 函數做爲調試用途,或者測試當前queue的標識。在block對象中調用這個函數會返回block提交到的queue(這個時候queue應該正在執行中)。在block對象以外調用這個函數會返回應用的默認併發queue。

使用 dispatch_get_main_queue 函數得到應用主線程關聯的串行dispatch queue。Cocoa 應用、調用了 dispatch_main 函數或配置了run loop(CFRunLoopRef 類型 或一個 NSRunLoop 對象)的應用,會自動建立這個queue。

使用 dispatch_get_global_queue 來得到共享的併發queue

Dispatch Queue的內存管理

Dispatch Queue和其它dispatch對象都是引用計數的數據類型。當你建立一個串行dispatch queue時,初始引用計數爲1,你可使用 dispatch_retain 和 dispatch_release 函數來增長和減小引用計數。當引用計數到達0時,系統會異步地銷燬這個queue。

對dispatch對象(如queue)retain和release是很重要的,確保它們被使用時可以保留在內存中。和內存託管的Cocoa對象同樣,通用的規則是若是你使用一個傳遞給你代碼中的queue,你應該在使用前retain,使用完以後release。

你不須要retain或release全局dispatch queue,包括全局併發 dispatch queue和main dispatch queue。

即便你實現的是自動垃圾收集的應用,也須要retain和release你的dispatch queue和其它dispatch對象。Grand Central Disaptch不支持垃圾收集模型來回收內存。

在Queue中存儲自定義上下文信息

全部dispatch對象(包括dispatch queue)都容許你關聯custom context data。使用 dispatch_set_context 和 dispatch_get_context 函數來設置和獲取對象的上下文數據。系統不會使用你的上下文數據,因此須要你本身在適當的時候分配和銷燬這些數據。

對於Queue,你可使用上下文數據來存儲一個指針,指向Objective-C對象或其它數據結構,協助標識這個queue或代碼的其它用途。你可使用queue的finalizer函數來銷燬(或解除關聯)上下文數據。

爲Queue提供一個清理函數

在建立串行dispatch queue以後,能夠附加一個finalizer函數,在queue被銷燬以前執行自定義的清理操做。使用 dispatch_set_finalizer_f 函數爲queue指定一個清理函數,當queue的引用計數到達0時,就會執行該清理函數。你可使用清理函數來解除queue關聯的上下文數據,並且只有上下文指針不爲NULL時纔會調用這個清理函數。

下面例子演示了自定義finalizer函數的使用,你須要本身提供 myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函數,用於初始化和清理上下文數據。

  1. void myFinalizerFunction(void *context) 
  2.     MyDataContext* theData = (MyDataContext*)context; 
  3.   
  4.     // Clean up the contents of the structure 
  5.     myCleanUpDataContextFunction(theData); 
  6.   
  7.     // Now release the structure itself. 
  8.     free(theData); 
  9.   
  10. dispatch_queue_t createMyQueue() 
  11.     MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext)); 
  12.     myInitializeDataContextFunction(data); 
  13.   
  14.     // Create the queue and set the context data. 
  15.     dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL); 
  16.     if (serialQueue) 
  17.     { 
  18.         dispatch_set_context(serialQueue, data); 
  19.         dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction); 
  20.     } 
  21.   
  22.     return serialQueue; 

添加任務到Queue

要執行一個任務,你須要將它dispatch到一個適當的dispatch queue,你能夠同步或異步地dispatch一個任務,也能夠單個或按組來dispatch。一旦進入到queue,queue會負責儘快地執行你的任務。

添加單個任務到Queue

你能夠異步或同步地添加一個任務到Queue,儘量地使用 dispatch_async 或 dispatch_async_f 函數異步地dispatch任務。由於添加任務到Queue中時,沒法肯定這些代碼何時可以執行。所以異步地添加block或函數,可讓你當即調度這些代碼的執行,而後調用線程能夠繼續去作其它事情。

特別是應用主線程必定要異步地dispatch任務,這樣才能及時地響應用戶事件。

少數時候你可能但願同步地dispatch任務,以免競爭條件或其它同步錯誤。使用 dispatch_sync 和 dispatch_sync_f 函數同步地添加任務到Queue,這兩個函數會阻塞,直到相應任務完成執行。

絕對不要在任務中調用 dispatch_sync 或 dispatch_sync_f 函數,並同步dispatch新任務到當前正在執行的queue。對於串行queue這一點特別重要,由於這樣作確定會致使死鎖;而併發queue也應該避免這樣作。

  1. dispatch_queue_t myCustomQueue; 
  2. myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL); 
  3.   
  4. dispatch_async(myCustomQueue, ^{ 
  5.     printf("Do some work here.\n"); 
  6. }); 
  7.   
  8. printf("The first block may or may not have run.\n"); 
  9.   
  10. dispatch_sync(myCustomQueue, ^{ 
  11.     printf("Do some more work here.\n"); 
  12. }); 
  13. printf("Both blocks have completed.\n"); 

任務完成時執行Completion Block

dispatch到queue中的任務,一般與建立任務的代碼獨立運行。在任務完成時,應用可能但願獲得通知並使用任務完成的結果數據。在傳統的異步編程模型中,你可能會使用回調機制,不過dispatch queue容許你使用Completion Block。

Completion Block是你dispatch到queue的另外一段代碼,在原始任務完成時自動執行。調用代碼在啓動任務時經過參數提供Completion Block。任務代碼只須要在完成工做時提交指定的Block或函數到指定的queue。

下面代碼使用block實現了平均數,最後兩個參數容許調用方指定一個queue和報告結果的block。在平均數函數完成計算後,會傳遞結果到指定的block,並dispatch到指定的queue。爲了防止queue被過早地釋放,必須首先retain這個queue,而後在dispatch這個Completion Block以後,再release這個queue。

  1. void average_async(int *data, size_t len, 
  2.    dispatch_queue_t queue, void (^block)(int)) 
  3.    // Retain the queue provided by the user to make 
  4.    // sure it does not disappear before the completion 
  5.    // block can be called. 
  6.    dispatch_retain(queue); 
  7.   
  8.    // Do the work on the default concurrent queue and then 
  9.    // call the user-provided block with the results. 
  10.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
  11.       int avg = average(data, len); 
  12.       dispatch_async(queue, ^{ block(avg);}); 
  13.   
  14.       // Release the user-provided queue when done 
  15.       dispatch_release(queue); 
  16.    }); 

併發地執行Loop Iteration

若是你使用循環執行固定次數的迭代,併發dispatch queue可能會提升性能。例以下面for循環:

  1. for (i = 0; i < count; i++) { 
  2.    printf("%u\n",i); 

若是每次迭代執行的任務與其它迭代獨立無關,並且循環迭代執行順序也可有可無的話,你能夠調用 dispatch_apply 或 dispatch_apply_f 函數來替換循環。這兩個函數爲每次循環迭代將指定的block或函數提交到queue。當dispatch到併發queue時,就有可能同時執行多個循環迭代。

調用 dispatch_apply 或 dispatch_apply_f 時你能夠指定串行或併發queue。併發queue容許同時執行多個循環迭代,而串行queue就沒太大必要使用了。

和普通for循環同樣,dispatch_apply 和 dispatch_apply_f 函數也是在全部迭代完成以後纔會返回。所以在queue上下文執行的代碼中再次調用這兩個函數時,必須很是當心。若是你傳遞的參數是串行queue,並且正是執行當前代碼的Queue,就會產生死鎖。

另外這兩個函數還會阻塞當前線程,所以在主線程中調用這兩個函數一樣必須當心,可能會阻止事件處理循環並沒有法響應用戶事件。因此若是循環代碼須要必定的時間執行,你能夠考慮在另外一個線程中調用這兩個函數。

下面代碼使用 dispatch_apply 替換了for循環,你傳遞的block必須包含一個參數,用來標識當前循環迭代。第一次迭代這個參數值爲0,第二次時爲1,最後一次值爲count - 1。

  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2.   
  3. dispatch_apply(count, queue, ^(size_t i) { 
  4.    printf("%u\n",i); 
  5. }); 

循環迭代執行的工做量須要仔細平衡,太多的話會下降響應性;太少則會影響總體性能,由於調度的開銷大於實際執行代碼。

在主線程中執行任務

Grand Central Disaptch提供一個特殊dispatch queue,能夠在應用的主線程中執行任務。應用主線程設置了run loop(由CFRunLoopRef 類型或 NSRunLoop 對象管理),就會自動建立這個queue,而且自動drain。非Cocoa應用若是不顯式地設置run loop,就必須顯式地調用dispatch_main 函數來顯式地drain這個dispatch queue。不然雖然你能夠添加任務到queue,但任務永遠不會被執行。

調用 dispatch_get_main_queue 函數得到應用主線程的dispatch queue。添加到這個queue的任務由主線程串行化執行,所以你能夠在應用的某些地方使用這個queue做爲同步點。

任務中使用Objective-C對象

Grand Central Disaptch支持Cocoa內存管理機制,所以能夠在提交到queue的block中自由地使用Objective-C對象。每一個dispatch queue維護本身的autorelease pool確保釋放autorelease對象,可是queue不保證這些對象實際釋放的時間。在自動垃圾收集的應用中,Grand Central Disaptch會在垃圾收集系統中註冊本身建立的每一個線程。

若是應用消耗大量內存,而且建立大量autorelease對象,你須要建立本身的autorelease pool,用來及時地釋放再也不使用的對象。

掛起和繼續queue

咱們能夠暫停一個queue以阻止它執行block對象,使用 dispatch_suspend 函數掛起一個dispatch queue;使用 dispatch_resume 函數繼續dispatch queue。調用 dispatch_suspend 會增長queue的引用計數,調用 dispatch_resume 則減小queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。所以你必須對應地調用suspend和resume函數。

掛起和繼續是異步的,並且只在執行block之間生效。掛起一個queue不會致使正在執行的block中止。

使用Dispatch Semaphore控制有限資源的使用

若是提交到dispatch queue中的任務須要訪問某些有限資源,可使用dispatch semaphore來控制同時訪問這個資源的任務數量。dispatch semaphore和普通的信號量相似,惟一的區別是當資源可用時,須要更少的時間來得到dispatch semaphore。

使用dispatch semaphore的過程以下:

使用 dispatch_semaphore_create 函數建立semaphore,指定正數值表示資源的可用數量。

在每一個任務中,調用 dispatch_semaphore_wait 來等待Semaphore

當上面調用返回時,得到資源並開始工做

使用完資源後,調用 dispatch_semaphore_signal 函數釋放和signal這個semaphore

  1. // Create the semaphore, specifying the initial pool size 
  2. dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2); 
  3.   
  4. // Wait for a free file descriptor 
  5. dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER); 
  6. fd = open("/etc/services", O_RDONLY); 
  7.   
  8. // Release the file descriptor when done 
  9. close(fd); 
  10. dispatch_semaphore_signal(fd_sema); 

等待queue中的一組任務

Dispatch group用來阻塞一個線程,直到一個或多個任務完成執行。有時候你必須等待任務完成的結果,而後才能繼續後面的處理。dispatch group也能夠替代線程join。

基本的流程是設置一個組,dispatch任務到queue,而後等待結果。你須要使用 dispatch_group_async 函數,會關聯任務到相關的組和queue。使用 dispatch_group_wait 等待一組任務完成。

  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2. dispatch_group_t group = dispatch_group_create(); 
  3.   
  4. // Add a task to the group 
  5. dispatch_group_async(group, queue, ^{ 
  6.    // Some asynchronous work 
  7. }); 
  8.   
  9. // Do some other work while the tasks execute. 
  10.   
  11. // When you cannot make any more forward progress, 
  12. // wait on the group to block the current thread. 
  13. dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
  14.   
  15. // Release the group when it is no longer needed. 
  16. dispatch_release(group); 

Dispatch Queue和線程安全性

使用Dispatch Queue實現應用併發時,也須要注意線程安全性:

Dispatch queue自己是線程安全的。換句話說,你能夠在應用的任意線程中提交任務到dispatch queue,不須要使用鎖或其它同步機制。

不要在執行任務代碼中調用 dispatch_sync 函數調度相同的queue,這樣作會死鎖這個queue。若是你須要dispatch到當前queue,須要使用 dispatch_async 函數異步調度

避免在提交到dispatch queue的任務中得到鎖,雖然在任務中使用鎖是安全的,但在請求鎖時,若是鎖不可用,可能會徹底阻塞串行queue。相似的,併發queue等待鎖也可能阻止其它任務的執行。若是代碼須要同步,就使用串行dispatch queue。

雖然能夠得到運行任務的底層線程的信息,最好不要這樣作。

 

 

Dispatch Sources

現代系統一般提供異步接口,容許應用向系統提交請求,而後在系統處理請求時應用能夠繼續處理本身的事情。Grand Central Dispatch正是基於這個基本行爲而設計,容許你提交請求,並經過block和dispatch queue報告結果。

dispatch source是基礎數據類型,協調特定底層系統事件的處理。Grand Central Dispatch支持如下dispatch source:

Timer dispatch source:按期產生通知

Signal dispatch source:UNIX信號到達時產生通知

Descriptor dispatch source:各類文件和socket操做的通知

數據可讀

數據可寫

文件在文件系統中被刪除、移動、重命名

文件元數據信息改變

Process dispatch source:進程相關的事件通知

當進程退出時

當進程發起fork或exec等調用

信號被遞送到進程

Mach port dispatch source:Mach相關事件的通知

Custom dispatch source:你本身定義並本身觸發

Dispatch source替代了異步回調函數,來處理系統相關的事件。當你配置一個dispatch source時,你指定要監測的事件、dispatch queue、以及處理事件的代碼(block或函數)。當事件發生時,dispatch source會提交你的block或函數到指定的queue去執行

和手工提交到queue的任務不一樣,dispatch source爲應用提供連續的事件源。除非你顯式地取消,dispatch source會一直保留與dispatch queue的關聯。只要相應的事件發生,就會提交關聯的代碼到dispatch queue去執行。

爲了防止事件積壓到dispatch queue,dispatch source實現了事件合併機制。若是新事件在上一個事件處理器出列並執行以前到達,dispatch source會將新舊事件的數據合併。根據事件類型的不一樣,合併操做可能會替換舊事件,或者更新舊事件的信息。

建立Dispatch Source

建立dispatch source須要同時建立事件源和dispatch source自己。事件源是處理事件所須要的native數據結構,例如基於描述符的dispatch source,你須要打開描述符;基於進程的事件,你須要得到目標程序的進程ID。

而後能夠以下建立相應的dispatch source:

使用 dispatch_source_create 函數建立dispatch source

配置dispatch source:

爲dispatch source設置一個事件處理器

對於定時器源,使用 dispatch_source_set_timer 函數設置定時器信息

爲dispatch source賦予一個取消處理器(可選)調用 dispatch_resume 函數開始處理事件因爲dispatch source必須進行額外的配置才能被使用,dispatch_source_create 函數返回的dispatch source將處於掛起狀態。此時dispatch source會接收事件,可是不會進行處理。這時候你能夠安裝事件處理器,並執行額外的配置。

編寫和安裝一個事件處理器

你須要定義一個事件處理器來處理事件,能夠是函數或block對象,並使用 dispatch_source_set_event_handler 或 dispatch_source_set_event_handler_f 安裝事件處理器。事件到達時,dispatch source會提交你的事件處理器到指定的dispatch queue,由queue執行事件處理器。

事件處理器的代碼負責處理全部到達的事件。若是事件處理器已經在queue中並等待處理已經到達的事件,若是此時又來了一個新事件,dispatch source會合並這兩個事件。事件處理器一般只能看到最新事件的信息,不過某些類型的dispatch source也能得到已經發生以及合併的事件信息。

若是事件處理器已經開始執行,一個或多個新事件到達,dispatch source會保留這些事件,直到前面的事件處理器完成執行。而後以新事件再次提交處理器到queue。

函數事件處理器有一個context指針指向dispatch source對象,沒有返回值。Block事件處理器沒有參數,也沒有返回值。

  1. // Block-based event handler  
  2. void (^dispatch_block_t)(void)  
  3.    
  4. // Function-based event handler  
  5. void (*dispatch_function_t)(void *)  

在事件處理器中,你能夠從dispatch source中得到事件的信息,函數處理器能夠直接使用參數指針,Block則必須本身捕獲到dispatch source指針,通常block定義時會自動捕獲到外部定義的全部變量。

  1. dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, 
  2.                                  myDescriptor, 0, myQueue); 
  3. dispatch_source_set_event_handler(source, ^{ 
  4.    // Get some data from the source variable, which is captured 
  5.    // from the parent context. 
  6.    size_t estimated = dispatch_source_get_data(source); 
  7.   
  8.    // Continue reading the descriptor... 
  9. }); 
  10. dispatch_resume(source); 

Block捕獲外部變量容許更大的靈活性和動態性。固然,在Block中這些變量默認是隻讀的,雖然可使用__block來修改捕獲的變量,可是你最好不要在事件處理器中這樣作。由於Dispatch source異步執行事件處理器,當事件處理器修改原始外部變量時,有可能這些變量已經不存在了。

下面是事件處理器可以得到的事件信息:

函數 描述
dispatch_source_get_handle 這個函數返回dispatch source管理的底層系統數據類型。

對於描述符dispatch source,函數返回一個int,表示關聯的描述符

對於信號dispatch source,函數返回一個int,表示最新事件的信號數值

對於進程dispatch source,函數返回一個pid_t數據結構,表示被監控的進程

對於Mach port dispatch source,函數返回一個 mach_port_t 數據結構

對於其它dispatch source,函數返回的值未定義
dispatch_source_get_data 這個函數返回事件關聯的全部未決數據。

對於從文件中讀取數據的描述符dispatch source,這個函數返回能夠讀取的字節數

對於向文件中寫入數據的描述符dispatch source,若是能夠寫入,則返回正數值

對於監控文件系統活動的描述符dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_vnode_flags_t 枚舉類型

對於進程dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_proc_flags_t 枚舉類型

對於Mach port dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_machport_flags_t 枚舉類型

對於自定義dispatch source,函數返回從現有數據建立的新數據,以及傳遞給 dispatch_source_merge_data 函數的新數據。
dispatch_source_get_mask 這個函數返回用來建立dispatch source的事件標誌

對於進程dispatch source,函數返回dispatch source接收到的事件掩碼,參考 dispatch_source_proc_flags_t 枚舉類型

對於發送權利的Mach port dispatch source,函數返回指望事件的掩碼,參考 dispatch_source_mach_send_flags_t 枚舉類型

對於自定義 「或」 的dispatch source,函數返回用來合併數據值的掩碼。

安裝一個取消處理器

取消處理器在dispatch soruce釋放以前執行清理工做。多數類型的dispatch source不須要取消處理器,除非你對dispatch source有自定義行爲須要在釋放時執行。可是使用描述符或Mach port的dispatch source必須設置取消處理器,用來關閉描述符或釋放Mach port。不然可能致使微妙的bug,這些結構體會被系統其它部分或你的應用在不經意間重用。

你能夠在任什麼時候候安裝取消處理器,但一般咱們在建立dispatch source時就會安裝取消處理器。使用 dispatch_source_set_cancel_handler 或 dispatch_source_set_cancel_handler_f 函數來設置取消處理器。

下面取消處理器關閉描述符:

  1. dispatch_source_set_cancel_handler(mySource, ^{ 
  2.    close(fd); // Close a file descriptor opened earlier. 
  3. }); 

修改目標Queue

在建立dispatch source時能夠指定一個queue,用來執行事件處理器和取消處理器。不過你也可使用 dispatch_set_target_queue 函數在任什麼時候候修改目標queue。修改queue能夠改變執行dispatch source事件的優先級。

修改dispatch source的目標queue是異步操做,dispatch source會盡量快地完成這個修改。若是事件處理器已經進入queue並等待處理,它會繼續在原來的Queue中執行。隨後到達的全部事件的處理器都會在後面修改的queue中執行。

關聯自定義數據到dispatch source

和Grand Central Dispatch的其它類型同樣,你可使用 dispatch_set_context 函數關聯自定義數據到dispatch source。使用context指針存儲事件處理器須要的任何數據。若是你在context指針中存儲了數據,你就應該安裝一個取消處理器,在dispatch source再也不須要時釋放這些context自定義數據。

若是你使用block實現事件處理器,你也能夠捕獲本地變量,並在Block中使用。雖然這樣也能夠代替context指針,可是你應該明智地使用Block捕獲變量。由於dispatch source長時間存在於應用中,Block捕獲指針變量時必須很是當心,由於指針指向的數據可能會被釋放,所以須要複製數據或retain。無論使用哪一種方法,你都應該提供一個取消處理器,在最後釋放這些數據。

Dispatch Source的內存管理

Dispatch Source也是引用計數的數據類型,初始計數爲1,可使用 dispatch_retain 和 dispatch_release 函數來增長和減小引用計數。引用計數到達0時,系統自動釋放dispatch source數據結構。

dispatch source的全部權能夠由dispatch source內部或外部進行管理。外部全部權時,另外一個對象擁有dispatch source,並負責在不須要時釋放它。內部全部權時,dispatch source本身擁有本身,並負責在適當的時候釋放本身。雖然外部全部權很經常使用,當你但願建立自主dispatch source,並讓它本身管理本身的行爲時,可使用內部全部權。例如dispatch source應用單一全局事件時,可讓它本身處理該事件,並當即退出。

Dispatch Source示例

建立一個定時器

定時器dispatch source定時產生事件,能夠用來發起定時執行的任務,如遊戲或其它圖形應用,可使用定時器來更新屏幕或動畫。你也能夠設置定時器,並在固定間隔事件中檢查服務器的新信息。

全部定時器dispatch source都是間隔定時器,一旦建立,會按你指定的間隔按期遞送事件。你須要爲定時器dispatch source指定一個指望的定時器事件精度,也就是leeway值,讓系統可以靈活地管理電源並喚醒內核。例如系統可使用leeway值來提早或延遲觸發定時器,使其更好地與其它系統事件結合。建立本身的定時器時,你應該儘可能指定一個leeway值。

就算你指定leeway值爲0,也不要指望定時器可以按照精確的納秒來觸發事件。系統會盡量地知足你的需求,可是沒法保證徹底精確的觸發時間。

當計算機睡眠時,定時器dispatch source會被掛起,稍後系統喚醒時,定時器dispatch source也會自動喚醒。根據你提供的配置,暫停定時器可能會影響定時器下一次的觸發。若是定時器dispatch source使用 dispatch_time 函數或 DISPATCH_TIME_NOW 常量設置,定時器dispatch source會使用系統默認時鐘來肯定什麼時候觸發,可是默認時鐘在計算機睡眠時不會繼續。

若是你使用 dispatch_walltime 函數來設置定時器dispatch source,則定時器會根據掛鐘時間來跟蹤,這種定時器比較適合觸發間隔相對比較大的場合,能夠防止定時器觸發間隔出現太大的偏差。

下面是定時器dispatch source的一個例子,每30秒觸發一次,leeway值爲1,由於間隔相對較大,使用 dispatch_walltime 來建立定時器。定時器會當即觸發第一次,隨後每30秒觸發一次。 MyPeriodicTask 和 MyStoreTimer 是自定義函數,用於實現定時器的行爲,並存儲定時器到應用的數據結構。

  1. dispatch_source_t CreateDispatchTimer(uint64_t interval, 
  2.               uint64_t leeway, 
  3.               dispatch_queue_t queue, 
  4.               dispatch_block_t block) 
  5.    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 
  6.                                                      0, 0, queue); 
  7.    if (timer) 
  8.    { 
  9.       dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway); 
  10.       dispatch_source_set_event_handler(timer, block); 
  11.       dispatch_resume(timer); 
  12.    } 
  13.    return timer; 
  14.   
  15. void MyCreateTimer() 
  16.    dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC, 
  17.                                1ull * NSEC_PER_SEC, 
  18.                                dispatch_get_main_queue(), 
  19.                                ^{ MyPeriodicTask(); }); 
  20.   
  21.    // Store it somewhere for later use. 
  22.     if (aTimer) 
  23.     { 
  24.         MyStoreTimer(aTimer); 
  25.     } 

雖然定時器dispatch source是接收時間事件的主要方法,你還可使用其它選擇。若是想在指定時間間隔後執行一個block,可使用 dispatch_after 或 dispatch_after_f 函數。這兩個函數很是相似於dispatch_async,可是隻容許你指定一個時間值,時間一到就自動提交block到queue中執行,時間值能夠指定爲相對或絕對時間。

從描述符中讀取數據

要從文件或socket中讀取數據,須要打開文件或socket,並建立一個 DISPATCH_SOURCE_TYPE_READ 類型的dispatch source。你指定的事件處理器必須可以讀取和處理描述符中的內容。對於文件,須要讀取文件數據,併爲應用建立適當的數據結構;對於網絡socket,須要處理最新接收到的網絡數據。

讀取數據時,你老是應該配置描述符使用非阻塞操做,雖然你可使用 dispatch_source_get_data 函數查看當前有多少數據可讀,但在你調用它和實際讀取數據之間,可用的數據數量可能會發生變化。若是底層文件被截斷,或發生網絡錯誤,從描述符中讀取會阻塞當前線程,中止在事件處理器中間並阻止dispatch queue去執行其它任務。對於串行queue,這樣還可能會死鎖,即便是併發queue,也會減小queue可以執行的任務數量。

下面例子配置dispatch source從文件中讀取數據,事件處理器讀取指定文件的所有內容到緩衝區,並調用一個自定義函數來處理這些數據。調用方可使用返回的dispatch source在讀取操做完成以後,來取消這個事件。爲了確保dispatch queue不會阻塞,這裏使用了fcntl函數,配置文件描述符執行非阻塞操做。dispatch source安裝了取消處理器,確保最後關閉了文件描述符。

  1. dispatch_source_t ProcessContentsOfFile(const char* filename) 
  2.    // Prepare the file for reading. 
  3.    int fd = open(filename, O_RDONLY); 
  4.    if (fd == -1) 
  5.       return NULL; 
  6.    fcntl(fd, F_SETFL, O_NONBLOCK);  // Avoid blocking the read operation 
  7.   
  8.    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  9.    dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, 
  10.                                    fd, 0, queue); 
  11.    if (!readSource) 
  12.    { 
  13.       close(fd); 
  14.       return NULL; 
  15.    } 
  16.   
  17.    // Install the event handler 
  18.    dispatch_source_set_event_handler(readSource, ^{ 
  19.       size_t estimated = dispatch_source_get_data(readSource) + 1; 
  20.       // Read the data into a text buffer. 
  21.       char* buffer = (char*)malloc(estimated); 
  22.       if (buffer) 
  23.       { 
  24.          ssize_t actual = read(fd, buffer, (estimated)); 
  25.          Boolean done = MyProcessFileData(buffer, actual);  // Process the data. 
  26.   
  27.          // Release the buffer when done. 
  28.          free(buffer); 
  29.   
  30.          // If there is no more data, cancel the source. 
  31.          if (done) 
  32.             dispatch_source_cancel(readSource); 
  33.       } 
  34.     }); 
  35.   
  36.    // Install the cancellation handler 
  37.    dispatch_source_set_cancel_handler(readSource, ^{close(fd);}); 
  38.   
  39.    // Start reading the file. 
  40.    dispatch_resume(readSource); 
  41.    return readSource; 

在這個例子中,自定義的 MyProcessFileData 函數肯定讀取到足夠的數據,返回YES告訴dispatch source讀取已經完成,能夠取消任務。一般讀取描述符的dispatch source在還有數據可讀時,會重複調度事件處理器。若是socket鏈接關閉或到達文件末尾,dispatch source自動中止調度事件處理器。若是你本身肯定再也不須要dispatch source,也能夠手動取消它。

向描述符寫入數據

向文件或socket寫入數據很是相似於讀取數據,配置描述符爲寫入操做後,建立一個 DISPATCH_SOURCE_TYPE_WRITE 類型的dispatch source,建立好以後,系統會調用事件處理器,讓它開始向文件或socket寫入數據。當你完成寫入後,使用 dispatch_source_cancel 函數取消dispatch source。

寫入數據也應該配置文件描述符使用非阻塞操做,雖然 dispatch_source_get_data 函數能夠查看當前有多少可用寫入空間,但這個值只是建議性的,並且在你執行寫入操做時可能會發生變化。若是發生錯誤,寫入數據到阻塞描述符,也會使事件處理器中止在執行中途,並阻止dispatch queue執行其它任務。串行queue會產生死鎖,併發queue則會減小可以執行的任務數量。

下面是使用dispatch source寫入數據到文件的例子,建立文件後,函數傳遞文件描述符到事件處理器。MyGetData函數負責提供要寫入的數據,在數據寫入到文件以後,事件處理器取消dispatch source,阻止再次調用。此時dispatch source的擁有者需負責釋放dispatch source。

  1. dispatch_source_t WriteDataToFile(const char* filename) 
  2.     int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 
  3.                       (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID)); 
  4.     if (fd == -1) 
  5.         return NULL; 
  6.     fcntl(fd, F_SETFL); // Block during the write. 
  7.   
  8.     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  9.     dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, 
  10.                             fd, 0, queue); 
  11.     if (!writeSource) 
  12.     { 
  13.         close(fd); 
  14.         return NULL; 
  15.     } 
  16.   
  17.     dispatch_source_set_event_handler(writeSource, ^{ 
  18.         size_t bufferSize = MyGetDataSize(); 
  19.         void* buffer = malloc(bufferSize); 
  20.   
  21.         size_t actual = MyGetData(buffer, bufferSize); 
  22.         write(fd, buffer, actual); 
  23.   
  24.         free(buffer); 
  25.   
  26.         // Cancel and release the dispatch source when done. 
  27.         dispatch_source_cancel(writeSource); 
  28.     }); 
  29.   
  30.     dispatch_source_set_cancel_handler(writeSource, ^{close(fd);}); 
  31.     dispatch_resume(writeSource); 
  32.     return (writeSource); 

監控文件系統對象

若是須要監控文件系統對象的變化,能夠設置一個 DISPATCH_SOURCE_TYPE_VNODE 類型的dispatch source,你能夠從這個dispatch source中接收文件刪除、寫入、重命名等通知。你還能夠獲得文件的特定元數據信息變化通知。

在dispatch source正在處理事件時,dispatch source中指定的文件描述符必須保持打開狀態。

下面例子監控一個文件的文件名變化,並在文件名變化時執行一些操做(自定義的 MyUpdateFileName 函數)。因爲文件描述符專門爲dispatch source打開,dispatch source安裝了取消處理器來關閉文件描述符。這個例子中的文件描述符關聯到底層的文件系統對象,所以同一個dispatch source能夠用來檢測屢次文件名變化。

  1. dispatch_source_t MonitorNameChangesToFile(const char* filename) 
  2.    int fd = open(filename, O_EVTONLY); 
  3.    if (fd == -1) 
  4.       return NULL; 
  5.   
  6.    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  7.    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, 
  8.                 fd, DISPATCH_VNODE_RENAME, queue); 
  9.    if (source) 
  10.    { 
  11.       // Copy the filename for later use. 
  12.       int length = strlen(filename); 
  13.       char* newString = (char*)malloc(length + 1); 
  14.       newString = strcpy(newString, filename); 
  15.       dispatch_set_context(source, newString); 
  16.   
  17.       // Install the event handler to process the name change 
  18.       dispatch_source_set_event_handler(source, ^{ 
  19.             const char*  oldFilename = (char*)dispatch_get_context(source); 
  20.             MyUpdateFileName(oldFilename, fd); 
  21.       }); 
  22.   
  23.       // Install a cancellation handler to free the descriptor 
  24.       // and the stored string. 
  25.       dispatch_source_set_cancel_handler(source, ^{ 
  26.           char* fileStr = (char*)dispatch_get_context(source); 
  27.           free(fileStr); 
  28.           close(fd); 
  29.       }); 
  30.   
  31.       // Start processing events. 
  32.       dispatch_resume(source); 
  33.    } 
  34.    else 
  35.       close(fd); 
  36.   
  37.    return source; 

監測信號

應用能夠接收許多不一樣類型的信號,如不可恢復的錯誤(非法指令)、或重要信息的通知(如子進程退出)。傳統編程中,應用使用 sigaction 函數安裝信號處理器函數,信號到達時同步處理信號。若是你只是想信號到達時獲得通知,並不想實際地處理該信號,可使用信號dispatch source來異步處理信號。

信號dispatch source不能替代 sigaction 函數提供的同步信號處理機制。同步信號處理器能夠捕獲一個信號,並阻止它停止應用。而信號dispatch source只容許你監測信號的到達。此外,你不能使用信號dispatch source獲取全部類型的信號,如SIGILL, SIGBUS, SIGSEGV信號。

因爲信號dispatch source在dispatch queue中異步執行,它沒有同步信號處理器的一些限制。例如信號dispatch source的事件處理器能夠調用任何函數。靈活性增大的代價是,信號到達和dispatch source事件處理器被調用的延遲可能會增大。

下面例子配置信號dispatch source來處理SIGHUP信號,事件處理器調用 MyProcessSIGHUP 函數,用來處理信號。

  1. void InstallSignalHandler() 
  2.    // Make sure the signal does not terminate the application. 
  3.    signal(SIGHUP, SIG_IGN); 
  4.   
  5.    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  6.    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue); 
  7.   
  8.    if (source) 
  9.    { 
  10.       dispatch_source_set_event_handler(source, ^{ 
  11.          MyProcessSIGHUP(); 
  12.       }); 
  13.   
  14.       // Start processing signals 
  15.       dispatch_resume(source); 
  16.    } 

監控進程

進程dispatch source能夠監控特定進程的行爲,並適當地響應。父進程可使用dispatch source來監控本身建立的全部子進程,例如監控子進程的死亡;相似地,子進程也可使用dispatch source來監控父進程,例如在父進程退出時本身也退出。

下面例子安裝了一個進程dispatch source,監控父進程的終止。當父進程退出時,dispatch source設置一些內部狀態信息,告知子進程本身應該退出。MySetAppExitFlag 函數應該設置一個適當的標誌,容許子進程終止。因爲dispatch source自主運行,所以本身擁有本身,在程序關閉時會取消並釋放本身。

  1. void MonitorParentProcess() 
  2.    pid_t parentPID = getppid(); 
  3.   
  4.    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  5.    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, 
  6.                                                       parentPID, DISPATCH_PROC_EXIT, queue); 
  7.    if (source) 
  8.    { 
  9.       dispatch_source_set_event_handler(source, ^{ 
  10.          MySetAppExitFlag(); 
  11.          dispatch_source_cancel(source); 
  12.          dispatch_release(source); 
  13.       }); 
  14.       dispatch_resume(source); 
  15.    } 

取消一個Dispatch Source

除非你顯式地調用 dispatch_source_cancel 函數,dispatch source將一直保持活動,取消一個dispatch source會中止遞送新事件,而且不能撤銷。所以你一般在取消dispatch source後當即釋放它:

  1. void RemoveDispatchSource(dispatch_source_t mySource) 
  2.    dispatch_source_cancel(mySource); 
  3.    dispatch_release(mySource); 

 

取消一個dispatch source是異步操做,調用 dispatch_source_cancel 以後,不會再有新的事件被處理,可是正在被dispatch source處理的事件會繼續被處理完成。在處理完最後的事件以後,dispatch source會執行本身的取消處理器。

取消處理器是你最後的執行機會,在那裏執行內存或資源的釋放工做。例如描述符或mach port類型的dispatch source,必須提供取消處理器,用來關閉描述符或mach port

掛起和繼續Dispatch Source

你可使用 dispatch_suspend 和 dispatch_resume 臨時地掛起和繼續dispatch source的事件遞送。這兩個函數分別增長和減小dispatch 對象的掛起計數。所以,你必須每次 dispatch_suspend 調用以後,都須要相應的 dispatch_resume 才能繼續事件遞送。

掛起一個dispatch source期間,發生的任何事件都會被累積,直到dispatch source繼續。可是不會遞送全部事件,而是先合併到單一事件,而後再一次遞送。例如你監控一個文件的文件名變化,就只會遞送最後一次的變化事件。

 

 

Migrating Away from Threads

從現有的線程代碼遷移到Grand Central Dispatch和Operation對象有許多方法,儘管可能不是全部線程代碼都可以執行遷移,可是遷移可能提高性能,並簡化你的代碼。

使用dispatch queue和Operaiton queue相比線程擁有許多優勢:

應用再也不須要存儲線程棧到內存空間

消除了建立和配置線程的代碼

消除了管理和調度線程工做的代碼

簡化了你要編寫的代碼

使用Dispatch Queue替代線程

首先考慮應用可能使用線程的幾種方式:

單一任務線程:建立一個線程執行單一任務,任務完成時釋放線程

工做線程(Worker):建立一個或多個工做線程執行特定的任務,按期地分配任務給每一個線程

線程池:建立一個通用的線程池,併爲每一個線程設置run loop,當你須要執行一個任務時,從池中抓取一個線程,並分配任務給它。若是沒有空閒線程可用,任務進入等待隊列。

雖然這些看上去是徹底不一樣的技術,但實際上只是相同原理的變種。應用都是使用線程來執行某些任務,區別在於管理線程和任務排隊的代碼。使用dispatch queue和operation queue,你能夠消除全部線程、及線程通訊的代碼,集中精力編寫處理任務的代碼。

若是你使用了上面的線程模型,你應該已經很是瞭解應用須要執行的任務類型,只須要封裝任務到Operation對象或Block對象,而後dispatch到適當的queue,就一切搞定!

對於那些不使用鎖的任務,你能夠直接使用如下方法來進行遷移:

單一任務線程,封裝任務到block或operation對象,並提交到併發queue

工做線程,首先你須要肯定使用串行queue仍是併發queue,若是工做線程須要同步特定任務的執行,就應該使用串行queue。若是工做線程只是執行任意任務,任務之間並沒有關聯,就應該使用併發queue

線程池,封裝任務到block或operation對象,並提交到併發queue中執行

固然,上面只是簡單的狀況。若是任務會爭奪共享資源,理想的解決方案固然是消除或最小化共享資源的爭奪。若是有辦法重構代碼,消除任務彼此對共享資源的依賴,這是最理想的。

若是作不到消除共享資源依賴,你仍然可使用queue,由於queue可以提供可預測的代碼執行順序。可預測意味着你不須要鎖或其它重量級的同步機制,就能夠實現代碼的同步執行。

你可使用queue來取代鎖執行如下任務:

若是任務必須按特定順序執行,提交到串行dispatch queue;若是你想使用Operation queue,就使用Operation對象依賴來確保這些對象的執行順序。

若是你已經使用鎖來保護共享資源,建立一個串行queue來執行任務並修改該資源。串行queue能夠替換現有的鎖,直接做爲同步機制使用。

若是你使用線程join來等待後臺任務完成,考慮使用dispatch group;也可使用一個 NSBlockOperation 對象,或者Operation對象依賴,一樣能夠達到group-completion的行爲。

若是你使用「生產者-消費者」模型來管理有限資源池,考慮使用 dispatch queue 來簡化「生產者-消費者」

若是你使用線程來讀取和寫入描述符,或者監控文件操做,使用dispatch source

記住queue不是替代線程的萬能藥!queue提供的異步編程模型適合於延遲可有可無的場合。雖然queue提供配置任務執行優先級的方法,但更高的優先級也不能確保任務必定能在特定時間獲得執行。所以線程仍然是實現最小延遲的適當選擇,例如音頻和視頻playback等場合。

消除基於鎖的代碼

在線程代碼中,鎖是傳統的多個線程之間同步資源的訪問機制。可是鎖的開銷自己比較大,線程還需等待鎖的釋放。

使用queue替代基於鎖的線程代碼,消除了鎖帶來的開銷,而且簡化了代碼編寫。你能夠將任務放到串行queue,來控制任務對共享資源的訪問。queue的開銷要遠遠小於鎖,由於將任務放入queue不須要陷入內核來得到mutex

將任務放入queue時,你作的主要決定是同步仍是異步,異步提交任務到queue讓當前線程繼續運行;同步提交任務則阻塞當前線程,直到任務執行完成。兩種機制各有各的用途,不過一般異步優先於同步。

實現異步鎖

異步鎖能夠保護共享資源,而又不阻塞任何修改資源的代碼。當代碼的部分工做須要修改一個數據結構時,可使用異步鎖。使用傳統的線程,你的實現方式是:得到共享資源的鎖,作必要的修改,釋放鎖,繼續任務的其它部分工做。可是使用dispatch queue,調用代碼能夠異步修改,無需等待這些修改操做完成。

下面是異步鎖實現的一個例子,受保護的資源定義了本身的串行dispatch queue。調用代碼提交一個block到這個queue,在block中執行對資源的修改。因爲queue串行的執行全部block,對這個資源的修改能夠確保按順序進行;並且因爲任務是異步執行的,調用線程不會阻塞。

dispatch_async(obj->serial_queue, ^{

// Critical section

});

同步執行臨界區

若是當前代碼必須等到指定任務完成,你可使用 dispatch_sync 函數同步的提交任務,這個函數將任務添加到dispatch queue,並阻塞當前線程直到任務完成執行。dispatch queue自己能夠是串行或併發queue,你能夠根據具體的須要來選擇使用。因爲 dispatch_sync 函數會阻塞當前線程,你只應該在確實須要的時候才使用。

下面是使用 dispatch_sync 實現臨界區的例子:

dispatch_sync(my_queue, ^{

// Critical section

});

若是你已經使用串行queue保護一個共享資源,同步提交到串行queue,並不能比異步提交提供更多的保護。同步提交的惟一理由是,阻止當前代碼在臨界區完成以前繼續執行。若是當前代碼不須要等待臨界區完成,或者能夠簡單的提交接下來的任務到相同的串行queue,就應該使用異步提交。

改進循環代碼

若是循環每次迭代執行的工做互相獨立,能夠考慮使用 dispatch_apply 或 dispatch_apply_f 函數來從新實現循環。這兩個函數將循環的每次迭代提交到dispatch queue進行處理。結合併發queue使用時,能夠併發地執行迭代以提升性能。

dispatch_apply 和 dispatch_apply_f 是同步函數,會阻塞當前線程直到全部循環迭代執行完成。當提交到併發queue時,循環迭代的執行順序是不肯定的。所以你用來執行循環迭代的Block對象(或函數)必須可重入(reentrant)。

下面例子使用dispatch來替換循環,你傳遞給 dispatch_apply 或 dispatch_apply_f 的Block或函數必須有一個整數參數,用來標識當前的循環迭代:

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {

printf("%u\n", i);

});

你須要明智地使用這項技術,由於dispatch queue的開銷雖然很是小,但仍然存在,你的循環代碼必須擁有足夠的工做量,才能忽略掉dispatch queue的這些開銷。

提高每次循環迭代工做量最簡單的辦法是striding(跨步),重寫block代碼執行多個循環迭代。從而減小了 dispatch_apply 函數指定的count值。

int stride = 137;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count / stride, queue, ^(size_t idx){

size_t j = idx * stride;

size_t j_stop = j + stride;

do {

printf("%u\n", (unsigned int)j++);

}while (j < j_stop);

});

// 執行剩餘的循環迭代

size_t i;

for (i = count - (count % stride); i < count; i++)

printf("%u\n", (unsigned int)i);

若是循環迭代次數很是多,使用stride能夠提高性能。

替換線程Join

線程join容許你生成多個線程,而後讓當前線程等待全部線程完成。線程建立子線程時指定爲joinable,若是父線程在子線程完成以前不能繼續處理,就能夠join子線程。join會阻塞父線程直到子線程完成任務並退出,這時候父線程能夠得到子線程的結果狀態,並繼續本身的工做。父線程能夠一次性join多個子線程。

Dispatch Group提供了相似於線程join的語義,但擁有更多優勢。dispatch group可讓線程阻塞直到一個或多個任務完成。和線程join不同的是,dispatch goup同時等待全部子任務完成。並且因爲dispatch group使用dispatch queue來執行任務,更加高效。

如下步驟可使用dispatch group替換線程join:

使用 dispatch_group_create 函數建立一個新的dispatch group

使用 dispatch_group_async 或 dispatch_group_async_f 函數添加任務到Group,這些是你要等待完成的任務

若是當前線程不能繼續處理任何工做,調用 dispatch_group_wait 函數等待這個group,會阻塞當前線程直到group中的全部任務執行完成。

若是你使用Operation對象來實現任務,可使用依賴來實現線程join。不過這時候不是讓父線程等待全部任務完成,而是將父代碼移到一個Operation對象,而後設置父Operation對象依賴於全部子Operation對象。這樣父Operation對象就會等到全部子Operation執行完成後纔開始執行。

修改「生產者-消費者」實現

生產者-消費者 模型能夠管理有限數量動態生產的資源。生產者生成新資源,消費者等待並消耗這些資源。實現生產者-消費者模型的典型機制是條件或信號量。

使用條件(Condition)時,生產者線程一般以下:

鎖住與condition關聯的mutex(使用pthread_mutex_lock)

生產資源(或工做)

Signal條件變量,通知有資源(或工做)能夠消費(使用pthread_cond_signal)

解鎖mutex(使用pthread_mutex_unlock)

對應的消費者線程則以下:

鎖住condition關聯的mutex(使用pthread_mutex_lock)

設置一個while循環[list=1]

檢查是否有資源(或工做)

若是沒有資源(或工做),調用pthread_cond_wait阻塞當前線程,直到相應的condition觸發

得到生產者提供的資源(或工做)解鎖mutex(使用pthread_mutex_unlock)處理資源(或工做)使用dispatch queue,你能夠簡化生產者-消費者爲一個調用:

dispatch_async(queue, ^{

// Process a work item.

});

當生產者有工做須要作時,只須要將工做添加到queue,並讓queue去處理該工做。惟一須要肯定的是queue的類型,若是生產者生成的任務須要按特定順序執行,就使用串行queue;不然使用併發Queue,讓系統儘量多地同時執行任務。

替換Semaphore代碼

使用信號量能夠限制對共享資源的訪問,你應該考慮使用dispatch semaphore來替換普通訊號量。傳統的信號量須要陷入內核,而dispatch semaphore能夠在用戶空間快速地測試狀態,只有測試失敗調用線程須要阻塞時纔會陷入內核。這樣dispatch semaphore擁有比傳統semaphore快得多的性能。二者的行爲是一致的。

替換Run-Loop代碼

若是你使用run loop來管理一個或多個線程執行的工做,你會發現使用queue來實現和維護任務會簡單許多。設置自定義run loop須要同時設置底層線程和run loop自己。run-loop代碼則須要設置一個或多個run loop source,並編寫回調來處理這些source事件到達。你能夠建立一個串行queue,並dispatch任務到queue中,這樣一行代碼就可以替換原有的run-loop建立代碼:

dispatch_queue_t myNewRunLoop = dispatch_queue_create("com.apple.MyQueue", NULL);

因爲queue自動執行添加進來的任務,不須要編寫額外的代碼來管理queue。你也不須要建立和配置線程,更不須要建立或附加任何run-loop source。此外,你能夠經過簡單地添加任務就能讓queue執行其它類型的任務,而run loop要實現這一點,必須修改現有run loop source,或者建立一個新的run loop source。

run loop的一個經常使用配置是處理網絡socket異步到達的數據,如今你能夠附加dispatch source到須要的queue中,來實現這個行爲。dispatch source還能提供更多處理數據的選項,支持更多類型的系統事件處理。

與POSIX線程的兼容性

Grand Central Dispatch管理了任務和運行線程之間的關係,一般你應該避免在任務代碼中使用POSIX線程函數,若是必定要使用,請當心。

應用不能刪除或mutate不是本身建立的數據結構。使用dispatch queue執行的block對象不能調用如下函數:

pthread_detach

pthread_cancel

pthread_join

pthread_kill

pthread_exit

任務運行時修改線程狀態是能夠的,但你必須還原線程原來的狀態。只要你記得還原線程的狀態,下面函數是安全的:

pthread_setcancelstate

pthread_setcanceltype

pthread_setschedparam

pthread_sigmask

pthread_setspecific

特定block的執行線程能夠在屢次調用間會發生變化,所以應用不該該依賴於如下函數返回的信息:

pthread_self

pthread_getschedparam

pthread_get_stacksize_np

pthread_get_stackaddr_np

pthread_mach_thread_np

pthread_from_mach_thread_np

pthread_getspecific

Block必須捕獲和禁止任何語言級的異常,Block執行期間的其它錯誤也應該由block處理,或者通知應用

相關文章
相關標籤/搜索