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最關鍵的優勢可以使用own lexical scope以外的變量,在函數或方法內部定義一個block時,block能夠直接讀取父scope中的變量。block訪問的變量所有被拷貝到block在堆中的數據結構,這樣block就能在稍後自由地訪問這些變量。當block被添加到dispatch queue中時,這些變量一般是隻讀格式的。不過同步執行的Block對象,可使用那些定義爲__block的變量,對這些變量的修改會影響到調用scope。
Block的簡單用法:
- int x = 123;
- int y = 456;
- // Block declaration and assignment
- void (^aBlock)(int) = ^(int z) {
- printf("%d %d %d\n", x, y, z);
- };
- // Execute the block
- 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:
- 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的名字,便於你跟蹤任務的執行。
- dispatch_queue_t queue;
- 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 函數,用於初始化和清理上下文數據。
- void myFinalizerFunction(void *context)
- {
- MyDataContext* theData = (MyDataContext*)context;
- // Clean up the contents of the structure
- myCleanUpDataContextFunction(theData);
- // Now release the structure itself.
- free(theData);
- }
- dispatch_queue_t createMyQueue()
- {
- MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext));
- myInitializeDataContextFunction(data);
- // Create the queue and set the context data.
- dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
- if (serialQueue)
- {
- dispatch_set_context(serialQueue, data);
- dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
- }
- 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也應該避免這樣作。
- dispatch_queue_t myCustomQueue;
- myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);
- dispatch_async(myCustomQueue, ^{
- printf("Do some work here.\n");
- });
- printf("The first block may or may not have run.\n");
- dispatch_sync(myCustomQueue, ^{
- printf("Do some more work here.\n");
- });
- 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。
- void average_async(int *data, size_t len,
- dispatch_queue_t queue, void (^block)(int))
- {
- // Retain the queue provided by the user to make
- // sure it does not disappear before the completion
- // block can be called.
- dispatch_retain(queue);
- // Do the work on the default concurrent queue and then
- // call the user-provided block with the results.
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- int avg = average(data, len);
- dispatch_async(queue, ^{ block(avg);});
- // Release the user-provided queue when done
- dispatch_release(queue);
- });
- }
併發地執行Loop Iteration
若是你使用循環執行固定次數的迭代,併發dispatch queue可能會提升性能。例以下面for循環:
- for (i = 0; i < count; i++) {
- 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。
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_apply(count, queue, ^(size_t i) {
- printf("%u\n",i);
- });
循環迭代執行的工做量須要仔細平衡,太多的話會下降響應性;太少則會影響總體性能,由於調度的開銷大於實際執行代碼。
在主線程中執行任務
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
- // Create the semaphore, specifying the initial pool size
- dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);
- // Wait for a free file descriptor
- dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
- fd = open("/etc/services", O_RDONLY);
- // Release the file descriptor when done
- close(fd);
- dispatch_semaphore_signal(fd_sema);
等待queue中的一組任務
Dispatch group用來阻塞一個線程,直到一個或多個任務完成執行。有時候你必須等待任務完成的結果,而後才能繼續後面的處理。dispatch group也能夠替代線程join。
基本的流程是設置一個組,dispatch任務到queue,而後等待結果。你須要使用 dispatch_group_async 函數,會關聯任務到相關的組和queue。使用 dispatch_group_wait 等待一組任務完成。
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_group_t group = dispatch_group_create();
- // Add a task to the group
- dispatch_group_async(group, queue, ^{
- // Some asynchronous work
- });
- // Do some other work while the tasks execute.
- // When you cannot make any more forward progress,
- // wait on the group to block the current thread.
- dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
- // Release the group when it is no longer needed.
- 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。
雖然能夠得到運行任務的底層線程的信息,最好不要這樣作。