在這兩部分的系列中,第一個部分的將解釋 GCD 是做什么的,并從許多基本的 GCD 函數中找出幾個來展示。在第二部分,你將學到幾個 GCD 提供的高級函數。
什么是 GCD
GCD 是 libdispatch 的市場名稱,而 libdispatch 作為 Apple 的一個庫,為并發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具有以下優點:
1.GCD 能通過推遲昂貴計算任務并在后臺運行它們來改善你的應用的響應性能。
2.GCD 提供一個易于使用的并發模型而不僅僅只是鎖和線程,以幫助我們避開并發陷阱。
3.GCD 具有在常見模式(例如單例)上用更高性能的原語優化你的代碼的潛在能力。
本教程假設你對 Block 和 GCD 有基礎了解。如果你對 GCD 完全陌生,先看看 iOS 上的多線程和 GCD 入門教程 學習其要領。
GCD 術語
要理解 GCD ,你要先熟悉與線程和并發相關的幾個概念。這兩者都可能模糊和微妙,所以在開始 GCD 之前先簡要地回顧一下它們。
Serial vs. Concurrent 串行 vs. 并發
這些術語描述當任務相對于其它任務被執行,任務串行執行就是每次只有一個任務被執行,任務并發執行就是在同一時間可以有多個任務被執行。
雖然這些術語被廣泛使用,本教程中你可以將任務設定為一個 Objective-C 的 Block 。不明白什么是 Block ?看看 iOS 5 教程中的如何使用 Block 。實際上,你也可以在 GCD 上使用函數指針,但在大多數場景中,這實際上更難于使用。Block 就是更加容易些!
Synchronous vs. Asynchronous 同步 vs. 異步
在 GCD 中,這些術語描述當一個函數相對于另一個任務完成,此任務是該函數要求 GCD 執行的。一個同步函數只在完成了它預定的任務后才返回。
一個異步函數,剛好相反,會立即返回,預定的任務會完成但不會等它完成。因此,一個異步函數不會阻塞當前線程去執行下一個函數。
注意——當你讀到同步函數“阻塞(Block)”當前線程,或函數是一個“阻塞”函數或阻塞操作時,不要被搞糊涂了!動詞“阻塞”描述了函數如何影響它所在的線程而與名詞“代碼塊(Block)”沒有關系。代碼塊描述了用 Objective-C 編寫的一個匿名函數,它能定義一個任務并被提交到 GCD 。
譯者注:中文不會有這個問題,“阻塞”和“代碼塊”是兩個詞。
Critical Section 臨界區
就是一段代碼不能被并發執行,也就是,兩個線程不能同時執行這段代碼。這很常見,因為代碼去操作一個共享資源,例如一個變量若能被并發進程訪問,那么它很可能會變質(譯者注:它的值不再可信)。
Race Condition 競態條件
這種狀況是指基于特定序列或時機的事件的軟件系統以不受控制的方式運行的行為,例如程序的并發任務執行的確切順序。競態條件可導致無法預測的行為,而不能通過代碼檢查立即發現。
Deadlock 死鎖
兩個(有時更多)東西——在大多數情況下,是線程——所謂的死鎖是指它們都卡住了,并等待對方完成或執行其它操作。第一個不能完成是因為它在等待第二個的完成。但第二個也不能完成,因為它在等待第一個的完成。
Thread Safe 線程安全
線程安全的代碼能在多線程或并發任務中被安全的調用,而不會導致任何問題(數據損壞,崩潰,等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary 。你可以在同一時間在多個線程中使用它而不會有問題。另一方面,NSMutableDictionary 就不是線程安全的,應該保證一次只能有一個線程訪問它。
Context Switch 上下文切換
一個上下文切換指當你在單個進程里切換執行不同的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很普遍,但會帶來一些額外的開銷。
Concurrency vs Parallelism 并發與并行
并發和并行通常被一起提到,所以值得花些時間解釋它們之間的區別。
并發代碼的不同部分可以“同步”執行。然而,該怎樣發生或是否發生都取決于系統。多核設備通過并行來同時執行多個線程;然而,為了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,然后運行另一個線程或進程。這通常發生地足夠快以致給我們并發執行地錯覺,如下圖所示:
雖然你可以編寫代碼在 GCD 下并發執行,但 GCD 會決定有多少并行的需求。并行要求并發,但并發并不能保證并行。
更深入的觀點是并發實際上是關于構造。當你在腦海中用 GCD 編寫代碼,你組織你的代碼來暴露能同時運行的多個工作片段,以及不能同時運行的那些。如果你想深入此主題,看看 this excellent talk by Rob Pike 。
Queues 隊列
GCD 提供有 dispatch queues 來處理代碼塊,這些隊列管理你提供給 GCD 的任務并用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列里的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。
所有的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程并行的訪問它們。 GCD 的優點是顯而易見的,即當你了解了調度隊列如何為你自己代碼的不同部分提供線程安全。關于這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工作。
在本節你會看到兩種調度隊列,都是由 GCD 提供的,然后看一些描述如何用調度函數添加工作到隊列的列子。
Serial Queues 串行隊列
這些任務的執行時機受到 GCD 的控制;唯一能確保的事情是 GCD 一次只執行一個任務,并且按照我們添加到隊列的順序來執行。
由于在串行隊列中不會有兩個任務并發運行,因此不會出現同時訪問臨界區的風險;相對于這些任務來說,這就從競態條件下保護了臨界區。所以如果訪問臨界區的唯一方式是通過提交到調度隊列的任務,那么你就不需要擔心臨界區的安全問題了。
Concurrent Queues 并發隊列
在并發隊列中的任務能得到的保證是它們會按照被添加的順序開始執行,但這就是全部的保證了。任務可能以任意順序完成,你不會知道何時開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這完全取決于 GCD 。
下圖展示了一個示例任務執行計劃,GCD 管理著四個并發任務:
注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始后,Block 1等待了好一會兒才開始。同樣, Block 3 在 Block 2 之后才開始,但它先于 Block 2 完成。
何時開始一個 Block 完全取決于 GCD 。如果一個 Block 的執行時間與另一個重疊,也是由 GCD 來決定是否將其運行在另一個不同的核心上,如果那個核心可用,否則就用上下文切換的方式來執行不同的 Block 。
有趣的是, GCD 提供給你至少五個特定的隊列,可根據隊列類型選擇使用。
Queue Types 隊列類型
首先,系統提供給你一個叫做 主隊列(main queue) 的特殊隊列。和其它串行隊列一樣,這個隊列中的任務一次只能執行一個。然而,它能保證所有的任務都在主線程執行,而主線程是唯一可用于更新 UI 的線程。這個隊列就是用于發生消息給 UIView 或發送通知的。
系統同時提供給你好幾個并發隊列。它們叫做 全局調度隊列(Global Dispatch Queues) 。目前的四個全局隊列有著不同的優先級:background、low、default 以及 high。要知道,Apple 的 API 也會使用這些隊列,所以你添加的任何任務都不會是這些隊列中唯一的任務。
最后,你也可以創建自己的串行隊列或并發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你自己創建的隊列。
以上是調度隊列的大框架!
dispatch queue分成以下三種:
1)運行在主線程的Main queue,通過dispatch_get_main_queue獲取。
復制代碼
/*!
* @function dispatch_get_main_queue
*
* @abstract
* Returns the default queue that is bound to the main thread.
*
* @discussion
* In order to invoke blocks submitted to the main queue, the application must
* call dispatch_main(), NSApplicationMain(), or use a CFRunLoop on the main
* thread.
*
* @result
* Returns the main queue. This queue is created automatically on behalf of
* the main thread before main() is called.
*/
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT struct dispatch_queue_s _dispatch_main_q;
#define dispatch_get_main_queue() \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q)
復制代碼
可以看出,dispatch_get_main_queue也是一種dispatch_queue_t。
2)并行隊列global dispatch queue,通過dispatch_get_global_queue獲取,由系統創建三個不同優先級的dispatch queue。并行隊列的執行順序與其加入隊列的順序相同。
3)串行隊列serial queues一般用于按順序同步訪問,可創建任意數量的串行隊列,各個串行隊列之間是并發的。
當想要任務按照某一個特定的順序執行時,串行隊列是很有用的。串行隊列在同一個時間只執行一個任務。我們可以使用串行隊列代替鎖去保護共享的數據。和鎖不同,一個串行隊列可以保證任務在一個可預知的順序下執行。
serial queues通過dispatch_queue_create創建,可以使用函數dispatch_retain和dispatch_release去增加或者減少引用計數。
GCD的用法:
復制代碼
//? 后臺執行:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// something
});
// 主線程執行:
dispatch_async(dispatch_get_main_queue(), ^{
// something
});
// 一次性執行:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
// 延遲2秒執行:
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// code to be executed on the main queue after delay
});
// 自定義dispatch_queue_t
dispatch_queue_t urls_queue = dispatch_queue_create("blog.devtang.com", NULL);
dispatch_async(urls_queue, ^{
// your code
});
dispatch_release(urls_queue);
// 合并匯總結果
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行執行的線程一
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行執行的線程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
// 匯總結果
});
復制代碼
一個應用GCD的例子:
復制代碼
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL * url = [NSURL URLWithString:@"http://www.baidu.com"];
NSError * error;
NSString * data = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
if (data != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"call back, the data is: %@", data);
});
} else {
NSLog(@"error when download:%@", error);
}
});
復制代碼
Dispatch Groups(調度組)
Dispatch Group 會在整個組的任務都完成時通知你。這些任務可以是同步的,也可以是異步的,即便在不同的隊列也行。而且在整個組的任務都完成時,Dispatch Group 可以用同步的或者異步的方式通知你。因為要監控的任務在不同隊列,那就用一個 dispatch_group_t 的實例來記下這些不同的任務。
當組中所有的事件都完成時,GCD 的 API 提供了兩種通知方式。
第一種是 dispatch_group_wait ,它會阻塞當前線程,直到組里面所有的任務都完成或者等到某個超時發生。這恰好是你目前所需要的。
打開 PhotoManager.m,用下列實現替換 downloadPhotosWithCompletionBlock:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create(); // 2
for (NSInteger i = 0; i < 3; i++) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup); // 3
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 4
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{ // 6
if (completionBlock) { // 7
completionBlock(error);
}
});
});
}
按照注釋的順序,你會看到:
1. 因為你在使用的是同步的 dispatch_group_wait ,它會阻塞當前線程,所以你要用 dispatch_async 將整個方法放入后臺隊列以避免阻塞主線程。
2. 創建一個新的 Dispatch Group,它的作用就像一個用于未完成任務的計數器。
3. dispatch_group_enter 手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對出現,否則你可能會遇到詭異的崩潰問題。
4. 手動通知 Group 它的工作已經完成。再次說明,你必須要確保進入 Group 的次數和離開 Group 的次數相等。
5. dispatch_group_wait 會一直等待,直到任務全部完成或者超時。如果在所有任務完成前超時了,該函數會返回一個非零值。你可以對此返回值做條件判斷以確定是否超出等待周期;然而,你在這里用 DISPATCH_TIME_FOREVER 讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,因為圖片的創建工作總是會完成的。
6. 此時此刻,你已經確保了,要么所有的圖片任務都已完成,要么發生了超時。然后,你在主線程上運行 completionBlock 回調。這會將工作放到主線程上,并在稍后執行。
7. 最后,檢查 completionBlock 是否為 nil,如果不是,那就運行它。
編譯并運行你的應用,嘗試下載多個圖片,觀察你的應用是在何時運行 completionBlock 的。
注意:如果你是在真機上運行應用,而且網絡活動發生得太快以致難以觀察 completionBlock 被調用的時刻,那么你可以在 Settings 應用里的開發者相關部分里打開一些網絡設置,以確保代碼按照我們所期望的那樣工作。只需去往 Network Link Conditioner 區,開啟它,再選擇一個 Profile,“Very Bad Network” 就不錯。
如果你是在模擬器里運行應用,你可以使用 來自 GitHub 的 Network Link Conditioner 來改變網絡速度。它會成為你工具箱中的一個好工具,因為它強制你研究你的應用在連接速度并非最佳的情況下會變成什么樣。
目前為止的解決方案還不錯,但是總體來說,如果可能,最好還是要避免阻塞線程。你的下一個任務是重寫一些方法,以便當所有下載任務完成時能異步通知你。
在我們轉向另外一種使用 Dispatch Group 的方式之前,先看一個簡要的概述,關于何時以及怎樣使用有著不同的隊列類型的 Dispatch Group :
1. 自定義串行隊列:它很適合當一組任務完成時發出通知。
2. 主隊列(串行):它也很適合這樣的情況。但如果你要同步地等待所有工作地完成,那你就不應該使用它,因為你不能阻塞主線程。然而,異步模型是一個很有吸引力的能用于在幾個較長任務(例如網絡調用)完成后更新 UI 的方式。
3. 并發隊列:它也很適合 Dispatch Group 和完成時通知。
Dispatch Group,第二種方式
上面的一切都很好,但在另一個隊列上異步調度然后使用 dispatch_group_wait 來阻塞實在顯得有些笨拙。是的,還有另一種方式……
在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的實現替換它:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
// 1
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup); // 2
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 3
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
if (completionBlock) {
completionBlock(error);
}
});
}
下面解釋新的異步方法如何工作:
1. 在新的實現里,因為你沒有阻塞主線程,所以你并不需要將方法包裹在 async 調用中。
2. 同樣的 enter 方法,沒做任何修改。
3. 同樣的 leave 方法,也沒做任何修改。
4. dispatch_group_notify 以異步的方式工作。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那么 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所需要的。
對于這個特定的工作,上面的處理明顯更清晰,而且也不會阻塞任何線程。
A B C D 4個并發下載任務,怎樣在第一時間知道任務全部完成?
dispatch_group 可以幫我們實現這樣的控制。
上代碼,看說明.
[objc] view plain copy
dispatch_group_t group = dispatch_group_create();
// 某個任務放進 group
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任務代碼1
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任務代碼2
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 任務全部完成處理
NSLog(@"isover");
});
創建一個任務組,然后將異步操作放進組里面,在最后用notify 告知所有任務完成,并做相應處理,一般來說都是在主線程里面刷新UI來提示用戶了。你如果不依賴UI放進子線程里面也是沒有問題的。當然group同步的方式還有其他
[objc] view plain copy
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i ++)
{
dispatch_group_enter(group);
// 任務代碼i 假定任務 是異步執行block回調
// block 回調執行
dispatch_group_leave(group);
// block 回調執行
}
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
// 主線程處理
});
首先我們異步執行,因為dispatch_group_wait函數是阻塞的,for里面安排了三個任務,這三個任務都是加載,在任務開始前 調用 enter,任務完成時調用leave,wait函數一直阻塞,直到它發現group里面的任務全部leave,它才放棄阻塞(任務全部完成),然后我們在主線程更新UI告知用戶
7.3、定時器
大多數情況下,對于定時事件,你會選擇NSTimer。定時器的GCD版本是底層的,它會給你更多控制權——但要小心使用。
需要特別重點指出的是,為了讓OS節省電量,需要為GCD的定時器接口指定一個低的誤差值。如果你不必要的指定了一個過低的誤差值,你將會浪費更多的電量。
這里我們設定了一個5秒的定時器,并允許有十分之一秒的誤差:
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
NSLog(@"Time flies.");
});
dispatch_time_t start
dispatch_source_set_timer(source, DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC,
100ull * NSEC_PER_MSEC);
self.source = source;
dispatch_resume(self.source);