本文是投稿文章,作者:east520
GCD全稱為Grand Central Dispatch,是libdispatch的市場名稱,而libdispatch是Apple的一個庫,其為並發代碼在iOS和OS X的多核硬件上執行提供支持。確切地說GCD是一套低層級的C API,通過 GCD,開發者只需要向隊列中添加一段代碼塊(block或C函數指針),而不需要直接和線程打交道。GCD在後端管理著一個線程池,它不僅決定著你的代碼塊將在哪個線程被執行,還根據可用的系統資源對這些線程進行管理。這樣通過GCD來管理線程,從而解決線程被創建的問題。
GCD的優勢
易用: GCD 提供一個易於使用的並發模型而不僅僅只是鎖和線程,以幫助我們避開並發陷阱,而且因為基於block,它能極為簡單得在不同代碼作用域之間傳遞上下文。
靈活: GCD 具有在常見模式上(比如鎖、單例),用更高性能的方法優化代碼,而且GCD能提供更多的控制權力以及大量的底層函數。
性能: GCD能自動根據系統負載來增減線程數量,這就減少了上下文切換以及增加了計算效率。
GCD相關概念
如果要深入了解GCD,還有一些概念是需要知道的。
Dispatch Objects?
盡管GCD是純C語言的,但它被組建成面向對象的風格。GCD對象被稱為dispatch object, 所有的dispatch objects都是OC對象.,就如其他OC對象一樣,當開啟了ARC(automatic reference counting)時,dispatch objects的retain和release都會自動執行。而如果是MRC的話,dispatch objects會使用dispatch_retain和dispatch_release這兩個方法來控制引用計數。
Serial & Concurrent
串行任務就是每次只有一個任務被執行,並發任務就是在同一時間可以有多個任務被執行。
Synchronous & Asynchronous
同步函數意思是在完成了它預定的任務後才返回,在任務執行時會阻塞當前線程。而異步函數則是任務會完成但不會等它完成,所以異步函數不會阻塞當前線程,會繼續去執行下一個函數。
Concurrency & Parallelism
並發的意思就是同時運行多個任務。這些任務可能是以在單核 CPU 上以分時(時間共享)的形式同時運行,也可能是在多核 CPU 上以真正的並行方式來運行。然後為了使單核設備也能實現這一點,並發任務必須先運行一個線程,執行一個上下文切換,然後運行另一個線程或進程。並行則是真正意思上的多任務同時運行。
Context Switch
Context Switch即上下文切換,一個上下文切換指當你在單個進程裡切換執行不同的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很普遍,但會帶來一些額外的開銷。
Dispatch Queues
GCD dispatch queues是一個強大的執行多任務的工具。Dispatch queue是一個對象,它可以接受任務,並將任務以先進先出(FIFO)的順序來執行。Dispatch queue可以並發的或串行的執行任意一個代碼塊,而且並發任務會像NSOperationQueue那樣基於系統負載來合適地並發進行,串行隊列同一時間則只執行單一任務。Dispatch queues內部使用的是線程,GCD 管理這些線程,並且使用Dispatch queues的時候,我們都不需要自己創建線程。Dispatch queues相對於和線程直接通信的代碼優勢是:Dispatch queues使用起來特別方便,執行任務更加有效率。
Queue Types
GCD有三種隊列類型:
類型並發隊列雖然是能同時執行多個任務,但這些任務仍然是按照先到先執行(FIFO)的順序來執行的。並發隊列會基於系統負載來合適地選擇並發執行這些任務。在iOS5之前,並發隊列一般指的就是全局隊列(Global queue),進程中存在四個全局隊列:高、中(默認)、低、後台四個優先級隊列,可以調用dispatch_get_global_queue函數傳入優先級來訪問隊列。而在iOS5之後,我們也可以用dispatch_queue_create,並指定隊列類型DISPATCH_QUEUE_CONCURRENT,來自己創建一個並發隊列。
與主線程功能相同。實際上,提交至main queue的任務會在主線程中執行。main queue可以調用dispatch_get_main_queue()來獲得。因為main queue是與主線程相關的,所以這是一個串行隊列。和其它串行隊列一樣,這個隊列中的任務一次只能執行一個。它能保證所有的任務都在主線程執行,而主線程是唯一可用於更新 UI 的線程。
Getting Started!
好了,在了解了上述概念後,我們可以正式投入GCD的懷抱了!
創建和管理隊列
當你決定添加一些任務到隊列中時,你需要決定該用那種類型的隊列,並且抉擇該如何使用他們。Dispatch queues可以串行或並發地執行這些任務,當你腦海裡有一個大概的思路去如何使用隊列時,你可以額快速地設置好隊列的屬性。接下來的部分,本文將告訴大家如何創建隊列和怎樣設置隊列的屬性。
獲取一個全局隊列
當我們需要同時執行多個任務時,並發隊列是非常有用的。並發隊列其實仍然還是一個隊列,它保留了隊列中的任務按先進先出(FIFO)的順序執行的特點。同時,一個並發隊列可以移除t它多余的任務,甚至這些任務之前還有未完成的任務。一個並發隊列中實際執行的任務數是由很多因素決定的,比如系統的內核數,其他串行隊列中任務的優先級,以及其他進程的工作狀態。
系統為每個程序提供了四種全局隊列,這些隊列中僅僅通過優先級加以區別,這四種類型分別是高、中(默認)、低、後台。因為這些隊列是全局的,所以大家不能直接創建它們,取而代之的是我們可以通過dispatch_get_global_queue這個方法來調用它們。
代碼示例:
dispatch_queue_t?aQueue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0); ???//全局隊列的四種類型 ???DISPATCH_QUEUE_PRIORITY_HIGH ???DISPATCH_QUEUE_PRIORITY_DEFAULT ???DISPATCH_QUEUE_PRIORITY_LOW ???DISPATCH_QUEUE_PRIORITY_BACKGROUND
正如大家所看到的,因為存在隊列的優先級,所以那些在高優先級隊列中的任務會比在默認或低優先級隊列中的任務要先執行,而默認級別隊列的優先級又高於低優先級隊列。注意,這裡有一個比較特殊的級別容易被忽視,DISPATCH_QUEUE_PRIORITY_BACKGROUND。被設置成後台級別的隊列,它會等待所有比它級別高的隊列中的任務執行完或CPU空閒的時候才會執行自己的任務。例如磁盤的讀寫操作非常耗時,如果我們不需要立即獲取到磁盤的數據,我們可以把讀寫任務放到後台隊列中,這樣讀寫任務只會在恰當的時候去執行而不會影響需要更改優先級的其他任務,整個程序也會更加有效率。
Note: 盡管dispatch queues是引用計數對象,但是我們不需要用retain和release來管理全局的並發隊列。因為全局隊列對於程序來說是全局的,retain和release會被全局隊列忽略。所以,我們不需要存儲這些隊列的引用數,僅僅只需要在任何要使用它們的地方,調用dispatch_get_global_queue這個方法即可。
創建串行隊列&並發隊列
當我們需要某些任務以指定的順序去執行時,串行隊列是一個非常好的選擇。一個串行隊列在同一時間裡只會執行一個任務,而且每次都只會從隊列的頭部把任務取出來執行。正因為如此,我們可以用串行隊列來替代鎖的操作,比如數據資源的同步或修改數據結構時。和鎖不同的是,串行隊列能保證任務都是在可預見的順序裡執行,而且一旦我們在一個串行隊列裡異步提交了任務,隊列就能永遠不發生死鎖。怎麼樣,是不是很棒,不過不像並發隊列,這些串行隊列是需要我們自己創建和管理的。
我們還可以在程序裡創建任意數量的隊列,不過值得注意的是,我們要盡量避免創建大量的串行隊列而目的僅僅是為了同時執行隊列中的這些任務。雖然GCD 通過創建所謂的線程池來大致匹配 CPU 內核數量,但是線程的創建並不是無代價的。每個線程都需要占用內存和內核資源。所以如果需要創建大量的並發任務,我們只需要把這些任務放到並發隊列中即可。
代碼示例:
//dispatch_queue_t ???//dispatch_queue_create(const?char?*label,?dispatch_queue_attr_t?attr); ??//串行隊列 ???dispatch_queue_t?serialQueue; ???serialQueue?=?dispatch_queue_create("com.example.SerialQueue",?NULL); ??//並發隊列 ??dispatch_queue_t?concurrentQueue; ???concurrentQueue?=?dispatch_queue_create("com.example.ConcurrentQueue",?DISPATCH_QUEUE_CONCURRENT);
NOTE: dispatch_queue_attr_t設置成NULL的時候默認代表串行。
獲取Main Queue
獲取主隊列的方法很簡單,如下所示:
dispatch_queue_t?mainQueue; mainQueue?=?dispatch_get_main_queue();
創建隊列的自定義上下文
所有的dispatch objects(包括dispatch queues)允許我們關聯自定義的上下文。我們可以通過使用 dispatch_set_context和dispatch_get_context這兩個方法,來為objects設定和獲取這些上下文數據。因為系統不會使用我們自定義的數據,所以我們需要在適當的時候生成和銷毀這些數據。對於隊列,我們可以使用上下文來為一個OC對象或其他數據結構存儲一個指針,以此來作為某個隊列的唯一標識。我們可以在隊列銷毀前並在隊列最後執行的方法中去銷毀上下文數據。
代碼示例:
void?myFinalizerFunction(void?*context) ??{ ??????MyDataContext*?theData?=?(MyDataContext*)context; ??????//?清除這個數據的內容 ??????myCleanUpDataContextFunction(theData); ??????//?釋放數據. ??????free(theData); ???} ??dispatch_queue_t?createMyQueue() ??{ ??????MyDataContext*??data?=?(MyDataContext*)?malloc(sizeof(MyDataContext)); ??????myInitializeDataContextFunction(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; ??}
添加任務到隊列
GCD有兩種方式來把任務添加到隊列中:異步和同步。一般情況下,使用dispatch_async和dispatch_async_f來執行異步操作,是比同步操作更好的選擇。當我們添加一個block對象或C函數到一個隊列中後就立即返回了,任務會在之後由 GCD 決定執行,以及任務什麼時候執行完我們是無法知道確定的。這樣的好處是,如果我們需要在後台執行一個基於網絡或 CPU 緊張的任務時就使用異步方法 ,這樣就不會阻塞當前線程。
盡管一般情況下,我們會優先選擇異步操作,但是在某些情況下,我們還是需要任務同步來執行。比如需要用同步操作來防止資源競爭或其他同步問題。這時,我們可以用 dispatch_sync和dispatch_sync_f方法來把任務添加到隊列中,這樣被添加的任務會阻塞當前線程,直到這些任務執行完。
代碼示例:
//代碼示例: //異步執行 dispatch_queue_t?myCustomQueue; myCustomQueue?=?dispatch_queue_create("com.example.MyCustomQueue",?NULL); dispatch_async(myCustomQueue,?^{ ????NSLog("Do?some?work?here."); }); //同步執行 dispatch_sync(myCustomQueue,?^{ ????NSLog("Do?some?more?work?here."); });
NOTE:dispatch_async在不同隊列類型執行的情況
自定義串行隊列:當你想串行執行後台任務並追蹤它時就是一個好選擇。這消除了資源爭用,因為你知道一次只有一個任務在執行。
主隊列:這是在一個並發隊列上完成任務後更新 UI 的一般選擇。
並發隊列:這是在後台執行非 UI 工作的一般選擇
任務執行完後添加一個完成塊(Completion Block)
通常來說,我們把任務添加到隊列後,一旦任務執行完,我們希望能得到通知並及時處理任務完成的結果。安裝傳統的異步開發流程,我們可以使用回調機制,或者在隊列中使用完成塊(Completion Block)。
一個Completion Block是在原任務完成後,我們給隊列添加的一個代碼塊。回調代碼的經典做法一般是在任務開始時,把completion block當成一個參數。需要我們做的只是把一個指定的block或函數,在指定的隊列完成時,提交給這個隊列即可。
下面是在一個計算平均值的函數,其利用了block方法來作為運算結果的回調。這個函數的最後參數queue、block,指定了一個queue和一個block,其在計算完數值結果後會把結果值傳給這個block,然後再把block分發到這個隊列(queue)中。注意,為了避免queue被提前釋放掉了,我們可以在函數執行開始階段為隊列retain,然後在completion block完成後再release隊列。
代碼示例:
void?average_async(int?*data,?size_t?len, ???dispatch_queue_t?queue,?void?(^block)(int)) { ???//?Retain?the?queue?以此確保在completion?block ???//?完成前不會被釋放掉 ???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?queue ??????dispatch_release(queue); ???}); }
並發執行迭代循環
在開發中,並發隊列能很好地提高效率,特別是當我們需要執行一個數據龐大的循環操作時。打個比方來說吧,我們需要執行一個for循環,每一次循環操作如下:
for?(i?=?0;?i?GCD提供了一個簡化方法叫做dispatch_apply,當我們把這個方法放到並發隊列中執行時,這個函數會調用單一block多次,並平行運算,然後等待所有運算結束。
代碼示例:
dispatch_queue_t?queue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0); dispatch_apply(count,?queue,?^(size_t?i)?{ ???NSLog("%d",i); });怎麼樣,是不是很棒,但是需要異步怎麼辦?dispatch_apply函數是沒有異步版本的。解決的方法是只要用dispatch_async函數將所有代碼推到後台就行了。
代碼示例:
dispatch_queue_t?queue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0); dispatch_async(queue,?^{ ????dispatch_apply(count,?queue,?^(size_t?i)?{ ???????NSLog("%d",i); ????}); });掛起和恢復隊列
有時候,我們不想讓隊列中的某些任務馬上執行,這時我們可以通過掛起操作來阻止一個隊列中將要執行的任務。當需要掛起隊列時,使用dispatch_suspend方法;恢復隊列時,使用dispatch_resume方法。調用dispatch_suspend會增加隊列掛起的引用計數,而調用dispatch_resume則會減少引用計數,當引用計數大於0時,隊列會保持掛起狀態。因此,這隊列的掛起和恢復中,我們需要小心使用以避免引用計數計算錯誤的出現。
代碼示例:
dispatch_queue_t?myQueue; myQueue?=?dispatch_queue_create("com.example.MyCustomQueue",?NULL); //掛起隊列 dispatch_suspend(myQueue); //恢復隊列 dispatch_resume(myQueue);NOTE:執行掛起操作不會對已經開始執行的任務起作用,它僅僅只會阻止將要進行但是還未開始的任務。
使用Dispatch Semaphores
信號量的作用是控制多個任務對有限數量資源的訪問。一個dispatch semaphore就像一個普通信號的例外。當資源可用時,獲取dispatch semaphore的時間比獲取傳統的系統信號量要更少。這是因為GCD不調用這個特殊情況下的內核。唯一的一次需要在內核中調用的情況是,當資源不可用且系統需要在停止你的線程直到獲取信號。舉例來說更容易理解,如果你創建了一個有著兩個資源的信號量,那同時最多只能有兩個線程可以訪問臨界區。其他想使用資源的線程必須在FIFO隊列裡等待。
常用的dispatch semaphore的語法:
當創建信號量(使用dispatch_semaphore_create方法),我們可以指定一個正整數,表示可用資源的數量。
在每一個任務裡,調用dispatch_semaphore_wait來等待信號量。
當等待調用返回時,獲取資源並做自己的工作。
當我們用到資源後,釋放掉它,然後通過調用dispatch_semaphore_signal方法來發出信號。
每一個應用都提供了有限的文件描述符來使用,如果我們需要處理一大堆的文件時,我們不想在運行文件描述符的時候同時打開很多文件。取而代之的是,我們可以用信號量來限制同一時間裡文件描述符的數量。下面就是為了實現此需求的簡單代碼:
//?創建一個信號量 dispatch_semaphore_t?fd_sema?=?dispatch_semaphore_create(getdtablesize()?/?2); //?等待一個空閒的文件描述符 dispatch_semaphore_wait(fd_sema,?DISPATCH_TIME_FOREVER); fd?=?open("/etc/services",?O_RDONLY); //?當完成時,釋放掉文件描述符 close(fd); dispatch_semaphore_signal(fd_sema);Dispatch Groups的使用
Dispatch groups是阻塞線程直到一個或多個任務完成的一種方式。在那些需要等待任務完成才能執行某個處理的時候,你可以使用這個方法。Dispatch Group會在整個組的任務都完成時通知你,這些任務可以是同步的,也可以是異步的,即便在不同的隊列也行。而且在整個組的任務都完成時,Dispatch Group可以用同步的或者異步的方式通知你。當group中所有的任務都完成時,GCD 提供了兩種通知方式。
1.dispatch_group_wait。它會阻塞當前線程,直到組裡面所有的任務都完成或者等到某個超時發生。
代碼示例:
dispatch_queue_t?queue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0); dispatch_group_t?group?=?dispatch_group_create(); //?添加隊列到組中 dispatch_group_async(group,?queue,?^{ //?一些異步操作 }); //如果在所有任務完成前超時了,該函數會返回一個非零值。 //你可以對此返回值做條件判斷以確定是否超出等待周期; dispatch_group_wait(group,?DISPATCH_TIME_FOREVER); //?不需要group後將做釋放操作 dispatch_release(group);2.dispatch_group_notify。它以異步的方式工作,當 Dispatch Group中沒有任何任務時,它就會執行其代碼,那麼 completionBlock便會運行。
代碼示例:
dispatch_queue_t?queue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0); dispatch_group_t?group?=?dispatch_group_create(); //?添加隊列到組中 dispatch_group_async(group,?queue,?^{ //?一些異步操作 }); dispatch_group_notify(group,?dispatch_get_main_queue(),?^{? if?(completionBlock)?{?completionBlock(error);?} });OK!以上即是GCD的一些基本用法。下一部分將是講解GCD的進階編程,敬請期待。
參考文獻:
Grand Central Dispatch (GCD) Reference
Concurrency Programming Guide
Grand Central Dispatch In-Depth: Part 1/2