什麼是GCD?
Grand Central Dispatch或者GCD,是一套低層API,提供了一種新的方法來進行併發程序編寫。從基本功能上講,GCD有點像NSOperationQueue,他們都允許程序將任務切分爲多個單一任務然後提交至工作隊列來併發地或者串行地執行。GCD比之NSOpertionQueue更底層更高效,並且它不是Cocoa框架的一部分。
除了代碼的平行執行能力,GCD還提供高度集成的事件控制系統。可以設置句柄來響應文件描述符、mach ports(Mach port 用於 OS X上的進程間通訊)、進程、計時器、信號、用戶生成事件。這些句柄通過GCD來併發執行。
GCD的API很大程度上基於block,當然,GCD也可以脫離block來使用,比如使用傳統c機制提供函數指針和上下文指針。實踐證明,當配合block使用時,GCD非常簡單易用且能發揮其最大能力。
你可以在Mac上敲命令“man dispatch”來獲取GCD的文檔。
爲何使用?
GCD提供很多超越傳統多線程編程的優勢:
- 易用: GCD比之thread跟簡單易用。由於GCD基於work unit而非像thread那樣基於運算,所以GCD可以控制諸如等待任務結束、監視文件描述符、週期執行代碼以及工作掛起等任務。基於block的血統導致它能極爲簡單得在不同代碼作用域之間傳遞上下文。
- 效率: GCD被實現得如此輕量和優雅,使得它在很多地方比之專門創建消耗資源的線程更實用且快速。這關係到易用性:導致GCD易用的原因有一部分在於你可以不用擔心太多的效率問題而僅僅使用它就行了。
- 性能: GCD自動根據系統負載來增減線程數量,這就減少了上下文切換以及增加了計算效率。
Dispatch Objects
儘管GCD是純c語言的,但它被組建成面向對象的風格。GCD對象被稱爲dispatch object。Dispatch object像Cocoa對象一樣是引用計數的。使用dispatch_release和dispatch_retain函數來操作dispatch object的引用計數來進行內存管理。但主意不像Cocoa對象,dispatch object並不參與垃圾回收系統,所以即使開啓了GC,你也必須手動管理GCD對象的內存。
Dispatch queues 和 dispatch sources(後面會介紹到)可以被掛起和恢復,可以有一個相關聯的任意上下文指針,可以有一個相關聯的任務完成觸發函數。可以查閱“man dispatch_object”來獲取這些功能的更多信息。
Dispatch Queues
GCD的基本概念就是dispatch queue。dispatch queue是一個對象,它可以接受任務,並將任務以先到先執行的順序來執行。dispatch queue可以是併發的或串行的。併發任務會像NSOperationQueue那樣基於系統負載來合適地併發進行,串行隊列同一時間只執行單一任務。
GCD中有三種隊列類型:
- The main queue: 與主線程功能相同。實際上,提交至main queue的任務會在主線程中執行。main queue可以調用dispatch_get_main_queue()來獲得。因爲main queue是與主線程相關的,所以這是一個串行隊列。
- Global queues: 全局隊列是併發隊列,並由整個進程共享。進程中存在三個全局隊列:高、中(默認)、低三個優先級隊列。可以調用dispatch_get_global_queue函數傳入優先級來訪問隊列。
- 用戶隊列: 用戶隊列 (GCD並不這樣稱呼這種隊列, 但是沒有一個特定的名字來形容這種隊列,所以我們稱其爲用戶隊列) 是用函數
dispatch_queue_create
創建的隊列. 這些隊列是串行的。正因爲如此,它們可以用來完成同步機制, 有點像傳統線程中的mutex。
創建隊列
要使用用戶隊列,我們首先得創建一個。調用函數dispatch_queue_create就行了。函數的第一個參數是一個標籤,這純是爲了debug。Apple建議我們使用倒置域名來命名隊列,比如“com.dreamingwish.subsystem.task”。這些名字會在崩潰日誌中被顯示出來,也可以被調試器調用,這在調試中會很有用。第二個參數目前還不支持,傳入NULL就行了。
提交 Job
向一個隊列提交Job很簡單:調用dispatch_async函數,傳入一個隊列和一個block。隊列會在輪到這個block執行時執行這個block的代碼。下面的例子是一個在後臺執行一個巨長的任務:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self goDoSomethingLongAndInvolved];
NSLog(@"Done doing something long and involved");
});
dispatch_async
函數會立即返回, block會在後臺異步執行。
當然,通常,任務完成時簡單地NSLog個消息不是個事兒。在典型的Cocoa程序中,你很有可能希望在任務完成時更新界面,這就意味着需要在主線程中執行一些代碼。你可以簡單地完成這個任務——使用嵌套的dispatch,在外層中執行後臺任務,在內層中將任務dispatch到main queue:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self goDoSomethingLongAndInvolved];
dispatch_async(dispatch_get_main_queue(), ^{
[textField setStringValue:@"Done doing something long and involved"];
});
});
還有一個函數叫dispatch_sync,它乾的事兒和dispatch_async相同,但是它會等待block中的代碼執行完成並返回。結合 __block類型修飾符,可以用來從執行中的block獲取一個值。例如,你可能有一段代碼在後臺執行,而它需要從界面控制層獲取一個值。那麼你可以使用dispatch_sync簡單辦到:
__block NSString *stringValue;
dispatch_sync(dispatch_get_main_queue(), ^{
// __block variables aren't automatically retained
// so we'd better make sure we have a reference we can keep
stringValue = [[textField stringValue] copy];
});
[stringValue autorelease];
// use stringValue in the background now
我們還可以使用更好的方法來完成這件事——使用更“異步”的風格。不同於取界面層的值時要阻塞後臺線程,你可以使用嵌套的block來中止後臺線程,然後從主線程中獲取值,然後再將後期處理提交至後臺線程:
dispatch_queue_t bgQueue = myQueue; dispatch_async(dispatch_get_main_queue(), ^{ NSString *stringValue = [[[textField stringValue] copy] autorelease]; dispatch_async(bgQueue, ^{ // use stringValue in the background now }); });
取決於你的需求,myQueue可以是用戶隊列也可以使全局隊列。
不再使用鎖(Lock)
用戶隊列可以用於替代鎖來完成同步機制。在傳統多線程編程中,你可能有一個對象要被多個線程使用,你需要一個鎖來保護這個對象:
NSLock *lock;
訪問代碼會像這樣:
- (id)something
{
id localSomething;
[lock lock];
localSomething = [[something retain] autorelease];
[lock unlock];
return localSomething;
}
- (void)setSomething:(id)newSomething
{
[lock lock];
if(newSomething != something)
{
[something release];
something = [newSomething retain];
[self updateSomethingCaches];
}
[lock unlock];
}
使用GCD,可以使用queue來替代:
dispatch_queue_t queue;
要用於同步機制,queue必須是一個用戶隊列,而非全局隊列,所以使用usingdispatch_queue_create
初始化一個。然後可以用dispatch_async
或者 dispatch_sync
將共享數據的訪問代碼封裝起來:
- (id)something
{
__block id localSomething;
dispatch_sync(queue, ^{
localSomething = [something retain];
});
return [localSomething autorelease];
}
- (void)setSomething:(id)newSomething
{
dispatch_async(queue, ^{
if(newSomething != something)
{
[something release];
something = [newSomething retain];
[self updateSomethingCaches];
}
});
}
值得注意的是dispatch queue是非常輕量級的,所以你可以大用特用,就像你以前使用lock一樣。
現在你可能要問:“這樣很好,但是有意思嗎?我就是換了點代碼辦到了同一件事兒。”
實際上,使用GCD途徑有幾個好處:
- 平行計算: 注意在第二個版本的代碼中,
-setSomething:是怎麼使用dispatch_async的。調用
-setSomething:會立即返回,然後這一大堆工作會在後臺執行。如果updateSomethingCaches是一個很費時費力的任務,且調用者將要進行一項處理器高負荷任務,那麼這樣做會很棒。
- 安全: 使用GCD,我們就不可能意外寫出具有不成對Lock的代碼。在常規Lock代碼中,我們很可能在解鎖之前讓代碼返回了。使用GCD,隊列通常持續運行,你必將歸還控制權。
- 控制: 使用GCD我們可以掛起和恢復dispatch queue,而這是基於鎖的方法所不能實現的。我們還可以將一個用戶隊列指向另一個dspatch queue,使得這個用戶隊列繼承那個dispatch queue的屬性。使用這種方法,隊列的優先級可以被調整——通過將該隊列指向一個不同的全局隊列,若有必要的話,這個隊列甚至可以被用來在主線程上執行代碼。
- 集成: GCD的事件系統與dispatch queue相集成。對象需要使用的任何事件或者計時器都可以從該對象的隊列中指向,使得這些句柄可以自動在該隊列上執行,從而使得句柄可以與對象自動同步。
總結
現在你已經知道了GCD的基本概念、怎樣創建dispatch queue、怎樣提交Job至dispatch queue以及怎樣將隊列用作線程同步。接下來我會向你展示如何使用GCD來編寫平行執行代碼來充分利用多核系統的性能^ ^。我還會討論GCD更深層的東西,包括事件系統和queue targeting。