iOS 併發編程(2)

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的同步也比基於鎖的線程同步機制更加高效。 app

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

dispatch queues的幾個關鍵點: async

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

系統決定了同時可以執行的任務數量,應用在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。

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

相關文章
相關標籤/搜索