轉 GCD

GCD 深刻理解:第一部分

本文翻譯自 http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1html

原做者:Derek Selanderios

譯者:@nixzhugit

雖然 GCD 已經出現過一段時間了,但不是每一個人都明瞭其主要內容。這是能夠理解的;併發一直很棘手,而 GCD 是基於 C 的 API ,它們就像一組尖銳的棱角戳進 Objective-C 的平滑世界。咱們將分兩個部分的教程來深刻學習 GCD 。github

在這兩部分的系列中,第一個部分的將解釋 GCD 是作什麼的,並從許多基本的 GCD 函數中找出幾個來展現。在第二部分,你將學到幾個 GCD 提供的高級函數。vim

什麼是 GCD

GCD 是 libdispatch 的市場名稱,而 libdispatch 做爲 Apple 的一個庫,爲併發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具備如下優勢:數組

  • GCD 能經過推遲昂貴計算任務並在後臺運行它們來改善你的應用的響應性能。
  • GCD 提供一個易於使用的併發模型而不只僅只是鎖和線程,以幫助咱們避開併發陷阱。
  • GCD 具備在常見模式(例如單例)上用更高性能的原語優化你的代碼的潛在能力。

本教程假設你對 Block 和 GCD 有基礎瞭解。若是你對 GCD 徹底陌生,先看看 iOS 上的多線程和 GCD 入門教程 學習其要領。xcode

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 併發與並行

併發和並行一般被一塊兒提到,因此值得花些時間解釋它們之間的區別。

併發代碼的不一樣部分能夠「同步」執行。然而,該怎樣發生或是否發生都取決於系統。多核設備經過並行來同時執行多個線程;然而,爲了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,而後運行另外一個線程或進程。這一般發生地足夠快以至給咱們併發執行地錯覺,以下圖所示:

Concurrency_vs_Parallelism

雖然你能夠編寫代碼在 GCD 下併發執行,但 GCD 會決定有多少並行的需求。並行要求併發,但併發並不能保證並行。

更深刻的觀點是併發其實是關於構造。當你在腦海中用 GCD 編寫代碼,你組織你的代碼來暴露能同時運行的多個工做片斷,以及不能同時運行的那些。若是你想深刻此主題,看看 這個由Rob Pike作的精彩的講座 。

Queues 隊列

GCD 提供有 dispatch queues 來處理代碼塊,這些隊列管理你提供給 GCD 的任務並用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列裏的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。

全部的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程並行的訪問它們。當你瞭解了調度隊列如何爲你本身代碼的不一樣部分提供線程安全後,GCD的優勢就是顯而易見的。關於這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工做。

在本節你會看到兩種調度隊列,都是由 GCD 提供的,而後看一些描述如何用調度函數添加工做到隊列的例子。

Serial Queues 串行隊列

串行隊列中的任務一次執行一個,每一個任務只在前一個任務完成時纔開始。並且,你不知道在一個 Block 結束和下一個開始之間的時間長度,以下圖所示:

Serial-Queue

這些任務的執行時機受到 GCD 的控制;惟一能確保的事情是 GCD 一次只執行一個任務,而且按照咱們添加到隊列的順序來執行。

因爲在串行隊列中不會有兩個任務併發運行,所以不會出現同時訪問臨界區的風險;相對於這些任務來講,這就從競態條件下保護了臨界區。因此若是訪問臨界區的惟一方式是經過提交到調度隊列的任務,那麼你就不須要擔憂臨界區的安全問題了。

Concurrent Queues 併發隊列

在併發隊列中的任務能獲得的保證是它們會按照被添加的順序開始執行,但這就是所有的保證了。任務可能以任意順序完成,你不會知道什麼時候開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這徹底取決於 GCD 。

下圖展現了一個示例任務執行計劃,GCD 管理着四個併發任務:

Concurrent-Queue

注意 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) 。目前的四個全局隊列有着不一樣的優先級:backgroundlowdefault 以及 high。要知道,Apple 的 API 也會使用這些隊列,因此你添加的任何任務都不會是這些隊列中惟一的任務。

最後,你也能夠建立本身的串行隊列或併發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你本身建立的隊列。

以上是調度隊列的大框架!

GCD 的「藝術」歸結爲選擇合適的隊列來調度函數以提交你的工做。體驗這一點的最好方式是走一遍下邊的列子,咱們沿途會提供一些通常性的建議。

入門

既然本教程的目標是優化且安全的使用 GCD 調用來自不一樣線程的代碼,那麼你將從一個近乎完成的叫作 GooglyPuff 的項目入手。

GooglyPuff 是一個沒有優化,線程不安全的應用,它使用 Core Image 的人臉檢測 API 來覆蓋一對曲棍球眼睛到被檢測到的人臉上。對於基本的圖像,能夠從相機膠捲選擇,或用預設好的URL從互聯網下載。

點擊此處下載項目

完成項目下載以後,將其解壓到某個方便的目錄,再用 Xcode 打開它並編譯運行。這個應用看起來以下圖所示:

Workflow

注意當你選擇 Le Internet 選項下載圖片時,一個 UIAlertView 過早地彈出。你將在本系列教程地第二部分修復這個問題。

這個項目中有四個有趣的類:

  • PhotoCollectionViewController:它是應用開始的第一個視圖控制器。它用縮略圖展現全部選定的照片。
  • PhotoDetailViewController:它執行添加曲棍球眼睛到圖像上的邏輯,並用一個 UIScrollView 來顯示結果圖片。
  • Photo:這是一個類簇,它根據一個 NSURL 的實例或一個 ALAsset 的實例來實例化照片。這個類提供一個圖像、縮略圖以及從 URL 下載的狀態。
  • PhotoManager:它管理全部 Photo 的實例.

用 dispatch_async 處理後臺任務

回到應用並從你的相機膠捲添加一些照片或使用 Le Internet 選項下載一些。

注意在按下 PhotoCollectionViewController 中的一個 UICollectionViewCell 到生成一個新的 PhotoDetailViewController 之間花了多久時間;你會注意到一個明顯的滯後,特別是在比較慢的設備上查看很大的圖。

在重載 UIViewController 的 viewDidLoad 時容易加入太多雜亂的工做(too much clutter),這一般會引發視圖控制器出現前更長的等待。若是可能,最好是卸下一些工做放到後臺,若是它們不是絕對必需要運行在加載時間裏。

這聽起來像是 dispatch_async 能作的事情!

打開 PhotoDetailViewController 並用下面的實現替換 viewDidLoad :

- (void)viewDidLoad
{   
    [super viewDidLoad]; NSAssert(_image, @"Image not set; required to use view controller"); self.photoImageView.image = _image; //Resize if neccessary to ensure it's not pixelated if (_image.size.height <= self.photoImageView.bounds.size.height && _image.size.width <= self.photoImageView.bounds.size.width) { [self.photoImageView setContentMode:UIViewContentModeCenter]; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; dispatch_async(dispatch_get_main_queue(), ^{ // 2 [self fadeInNewImage:overlayImage]; // 3 }); }); }

下面來講明上面的新代碼所作的事:

  1. 你首先將工做從主線程移到全局線程。由於這是一個 dispatch_async() ,Block 會被異步地提交,意味着調用線程地執行將會繼續。這就使得 viewDidLoad 更早地在主線程完成,讓加載過程感受起來更加快速。同時,一我的臉檢測過程會啓動並將在稍後完成。
  2. 在這裏,人臉檢測過程完成,並生成了一個新的圖像。既然你要使用此新圖像更新你的 UIImageView ,那麼你就添加一個新的 Block 到主線程。記住——你必須老是在主線程訪問 UIKit 的類。
  3. 最後,你用 fadeInNewImage: 更新 UI ,它執行一個淡入過程切換到新的曲棍球眼睛圖像。

編譯並運行你的應用;選擇一個圖像而後你會注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在以後就加上了。這給應用帶來了不錯的效果,和以前的顯示差異巨大。

進一步,若是你試着加載一個超大的圖像,應用不會在加載視圖控制器上「掛住」,這就使得應用具備很好伸縮性。

正如以前提到的, dispatch_async 添加一個 Block 到隊列就當即返回了。任務會在以後由 GCD 決定執行。當你須要在後臺執行一個基於網絡或 CPU 緊張的任務時就使用 dispatch_async ,這樣就不會阻塞當前線程。

下面是一個關於在 dispatch_async 上如何以及什麼時候使用不一樣的隊列類型的快速指導:

  • 自定義串行隊列:當你想串行執行後臺任務並追蹤它時就是一個好選擇。這消除了資源爭用,由於你知道一次只有一個任務在執行。注意若你須要來自某個方法的數據,你必須內聯另外一個 Block 來找回它或考慮使用 dispatch_sync
  • 主隊列(串行):這是在一個併發隊列上完成任務後更新 UI 的共同選擇。要這樣作,你將在一個 Block 內部編寫另外一個 Block 。以及,若是你在主隊列調用 dispatch_async 到主隊列,你能確保這個新任務將在當前方法完成後的某個時間執行。
  • 併發隊列:這是在後臺執行非 UI 工做的共同選擇。

使用 dispatch_after 延後工做

稍微考慮一下應用的 UX 。是否用戶第一次打開應用時會困惑於不知道作什麼?你是這樣嗎? :]

若是用戶的 PhotoManager 裏尚未任何照片,那麼顯示一個提示會是個好主意!然而,你一樣要考慮用戶的眼睛會如何在主屏幕上瀏覽:若是你太快的顯示一個提示,他們的眼睛還徘徊在視圖的其它部分上,他們極可能會錯過它。

顯示提示以前延遲一秒鐘就足夠捕捉到用戶的注意,他們此時已經第一次看過了應用。

添加以下代碼到到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的廢止實現裏:

- (void)showOrHideNavPrompt
{
    NSUInteger count = [[PhotoManager sharedManager] photos].count; double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 if (!count) { [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; } else { [self.navigationItem setPrompt:nil]; } }); }

showOrHideNavPrompt 在 viewDidLoad 中執行,以及 UICollectionView 被從新加載的任什麼時候候。按照註釋數字順序看看:

  1. 你聲明瞭一個變量指定要延遲的時長。
  2. 而後等待 delayInSeconds 給定的時長,再異步地添加一個 Block 到主線程。

編譯並運行應用。應該有一個輕微地延遲,這有助於抓住用戶的注意力並展現所要作的事情。

dispatch_after 工做起來就像一個延遲版的 dispatch_async 。你依然不能控制實際的執行時間,且一旦 dispatch_after 返回也就不能再取消它。

不知道什麼時候適合使用 dispatch_after ?

  • 自定義串行隊列:在一個自定義串行隊列上使用 dispatch_after 要當心。你最好堅持使用主隊列。
  • 主隊列(串行):是使用 dispatch_after 的好選擇;Xcode 提供了一個不錯的自動完成模版。
  • 併發隊列:在併發隊列上使用 dispatch_after 也要當心;你會這樣作就比較罕見。仍是在主隊列作這些操做吧。

讓你的單例線程安全

單例,不論喜歡仍是討厭,它們在 iOS 上的流行狀況就像網上的貓。 :]

一個常見的擔心是它們經常不是線程安全的。這個擔心十分合理,基於它們的用途:單例經常被多個控制器同時訪問。

單例的線程擔心範圍從初始化開始,到信息的讀和寫。PhotoManager 類被實現爲單例——它在目前的狀態下就會被這些問題所困擾。要看看事情如何很快地失去控制,你將在單例實例上建立一個控制好的競態條件。

導航到 PhotoManager.m 並找到 sharedManager ;它看起來以下:

+ (instancetype)sharedManager    
{
    static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }

當前狀態下,代碼至關簡單;你建立了一個單例並初始化一個叫作 photosArray 的 NSMutableArray 屬性。

然而,if 條件分支不是線程安全的;若是你屢次調用這個方法,有一個可能性是在某個線程(就叫它線程A)上進入 if 語句塊並可能在 sharedPhotoManager 被分配內存前發生一個上下文切換。而後另外一個線程(線程B)可能進入 if ,分配單例實例的內存,而後退出。

當系統上下文切換回線程A,你會分配另一個單例實例的內存,而後退出。在那個時間點,你有了兩個單例的實例——很明顯這不是你想要的(譯者注:這還能叫單例嗎?)!

要強制這個(競態)條件發生,替換 PhotoManager.m 中的 sharedManager 爲下面的實現:

+ (instancetype)sharedManager  
{
    static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }

上面的代碼中你用 NSThread 的 sleepForTimeInterval: 類方法來強制發生一個上下文切換。

打開 AppDelegate.m 並添加以下代碼到 application:didFinishLaunchingWithOptions: 的最開始處:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; });

這裏建立了多個異步併發調用來實例化單例,而後引起上面描述的競態條件。

編譯並運行項目;查看控制檯輸出,你會看到多個單例被實例化,以下所示:

NSLog-Race-Condition

注意到這裏有好幾行顯示着不一樣地址的單例實例。這明顯違背了單例的目的,對吧?:]

這個輸出向你展現了臨界區被執行屢次,而它只應該執行一次。如今,當然是你本身強制這樣的情況發生,但你能夠想像一下這個情況會怎樣在無心間發生。

注意:基於其它你沒法控制的系統事件,NSLog 的數量有時會顯示多個。線程問題極其難以調試,由於它們每每難以重現。

要糾正這個情況,實例化代碼應該只執行一次,並阻塞其它實例在 if 條件的臨界區運行。這恰好就是 dispatch_once 能作的事。

在單例初始化方法中用 dispatch_once 取代 if 條件判斷,以下所示:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }

編譯並運行你的應用;查看控制檯輸出,你會看到有且僅有一個單例的實例——這就是你對單例的指望!:]

如今你已經明白了防止競態條件的重要性,從 AppDelegate.m 中移除 dispatch_async 語句,並用下面的實現替換 PhotoManager 單例的初始化:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }

dispatch_once() 以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once 的代碼)的不一樣的線程會在臨界區已有一個線程的狀況下被阻塞,直到臨界區完成爲止。

Highlander_dispatch_once

須要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類自己線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的狀況。這些須要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。

處理讀者與寫者問題

線程安全實例不是處理單例時的惟一問題。若是單例屬性表示一個可變對象,那麼你就須要考慮是否那個對象自身線程安全。

若是問題中的這個對象是一個 Foundation 容器類,那麼答案是——「極可能不安全」!Apple 維護一個有用且有些心寒的列表,衆多的 Foundation 類都不是線程安全的。 NSMutableArray,已用於你的單例,正在那個列表裏休息。

雖然許多線程能夠同時讀取 NSMutableArray 的一個實例而不會產生問題,但當一個線程正在讀取時讓另一個線程修改數組就是不安全的。你的單例在目前的情況下不能預防這種狀況的發生。

要分析這個問題,看看 PhotoManager.m 中的 addPhoto:,轉載以下:

- (void)addPhoto:(Photo *)photo
{
    if (photo) { [_photosArray addObject:photo]; dispatch_async(dispatch_get_main_queue(), ^{ [self postContentAddedNotification]; }); } }

這是一個方法,它修改一個私有可變數組對象。

如今看看 photos ,轉載以下:

- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray]; }

這是所謂的方法,它讀取可變數組。它爲調用者生成一個不可變的拷貝,防止調用者不當地改變數組,但這不能提供任何保護來對抗當一個線程調用讀方法 photos 的同時另外一個線程調用寫方法 addPhoto: 。

這就是軟件開發中經典的讀者寫者問題。GCD 經過用 dispatch barriers 建立一個讀者寫者鎖 提供了一個優雅的解決方案。

Dispatch barriers 是一組函數,在併發隊列上工做時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上惟一被執行的條目。這就意味着全部的先於調度障礙提交到隊列的條目必能在這個 Block 執行前完成。

當這個 Block 的時機到達,調度障礙執行這個 Block 並確保在那個時間裏隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。

下圖顯示了障礙函數對多個異步隊列的影響:

Dispatch-Barrier

注意到正常部分的操做就如同一個正常的併發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是惟一在執行的事物。在障礙完成後,隊列回到一個正常併發隊列的樣子。

下面是你什麼時候會——和不會——使用障礙函數的狀況:

  • 自定義串行隊列:一個很壞的選擇;障礙不會有任何幫助,由於無論怎樣,一個串行隊列一次都只執行一個操做。
  • 全局併發隊列:要當心;這可能不是最好的主意,由於其它系統可能在使用隊列並且你不能壟斷它們只爲你本身的目的。
  • 自定義併發隊列:這對於原子或臨界區代碼來講是極佳的選擇。任何你在設置或實例化的須要線程安全的事物都是使用障礙的最佳候選。

因爲上面惟一像樣的選擇是自定義併發隊列,你將建立一個你本身的隊列去處理你的障礙函數並分開讀和寫函數。且這個併發隊列將容許多個多操做同時進行。

打開 PhotoManager.m,添加以下私有屬性到類擴展中:

@interface PhotoManager () @property (nonatomic,strong,readonly) NSMutableArray *photosArray; @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this @end

找到 addPhoto: 並用下面的實現替換它:

- (void)addPhoto:(Photo *)photo
{
    if (photo) { // 1 dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2 [_photosArray addObject:photo]; // 3 dispatch_async(dispatch_get_main_queue(), ^{ // 4 [self postContentAddedNotification]; }); }); } }

你新寫的函數是這樣工做的:

  1. 在執行下面全部的工做前檢查是否有合法的相片。
  2. 添加寫操做到你的自定義隊列。當臨界區在稍後執行時,這將是你隊列中惟一執行的條目。
  3. 這是添加對象到數組的實際代碼。因爲它是一個障礙 Block ,這個 Block 永遠不會同時和其它 Block 一塊兒在 concurrentPhotoQueue 中執行。
  4. 最後你發送一個通知說明完成了添加圖片。這個通知將在主線程被髮送由於它將會作一些 UI 工做,因此在此爲了通知,你異步地調度另外一個任務到主線程。

這就處理了寫操做,但你還須要實現 photos 讀方法並實例化 concurrentPhotoQueue 。

在寫者打擾的狀況下,要確保線程安全,你須要在 concurrentPhotoQueue 隊列上執行讀操做。既然你須要從函數返回,你就不能異步調度到隊列,由於那樣在讀者函數返回以前不必定運行。

在這種狀況下,dispatch_sync 就是一個絕好的候選。

dispatch_sync() 同步地提交工做並在返回前等待它完成。使用 dispatch_sync 跟蹤你的調度障礙工做,或者當你須要等待操做完成後才能使用 Block 處理過的數據。若是你使用第二種狀況作事,你將不時看到一個 __block 變量寫在 dispatch_sync 範圍以外,以便返回時在 dispatch_sync 使用處理過的對象。

但你須要很當心。想像若是你調用 dispatch_sync 並放在你已運行着的當前隊列。這會致使死鎖,由於調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務沒法完成!這將迫使你自覺於你正從哪一個隊列調用——以及你正在傳遞進入哪一個隊列。

下面是一個快速總覽,關於在什麼時候以及何處使用 dispatch_sync :

  • 自定義串行隊列:在這個情況下要很是當心!若是你正運行在一個隊列並調用 dispatch_sync 放在同一個隊列,那你就百分百地建立了一個死鎖。
  • 主隊列(串行):同上面的理由同樣,必須很是當心!這個情況一樣有潛在的致使死鎖的狀況。
  • 併發隊列:這纔是作同步工做的好選擇,不管是經過調度障礙,或者須要等待一個任務完成才能執行進一步處理的狀況。

繼續在 PhotoManager.m 上工做,用下面的實現替換 photos :

- (NSArray *)photos
{
    __block NSArray *array; // 1 dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 array = [NSArray arrayWithArray:_photosArray]; // 3 }); return array; }

這就是你的讀函數。按順序看看編過號的註釋,有這些:

  1. __block 關鍵字容許對象在 Block 內可變。沒有它,array 在 Block 內部就只是只讀的,你的代碼甚至不能經過編譯。
  2. 在 concurrentPhotoQueue 上同步調度來執行讀操做。
  3. 將相片數組存儲在 array 內並返回它。

最後,你須要實例化你的 concurrentPhotoQueue 屬性。修改 sharedManager 以便像下面這樣初始化隊列:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; // ADD THIS: sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT); }); return sharedPhotoManager; }

這裏使用 dispatch_queue_create 初始化 concurrentPhotoQueue 爲一個併發隊列。第一個參數是反向DNS樣式命名慣例;確保它是描述性的,將有助於調試。第二個參數指定你的隊列是串行仍是併發。

注意:當你在網上搜索例子時,你會常常看人們傳遞 0 或者 NULL 給 dispatch_queue_create 的第二個參數。這是一個建立串行隊列的過期方式;明確你的參數老是更好。

恭喜——你的 PhotoManager 單例如今是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。

A Visual Review of Queueing 隊列的虛擬回顧

依然沒有 100% 地掌握 GCD 的要領?確保你可使用 GCD 函數輕鬆地建立簡單的例子,使用斷點和 NSLog 語句保證本身明白當下發生的狀況。

我在下面提供了兩個 GIF動畫來幫助你鞏固對 dispatch_async 和 dispatch_sync 的理解。包含在每一個 GIF 中的代碼能夠提供視覺輔助;仔細注意 GIF 左邊顯示代碼斷點的每一步,以及右邊相關隊列的狀態。

dispatch_sync 回顧

- (void)viewDidLoad
{
  [super viewDidLoad]; dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }

dispatch_sync_in_action

下面是圖中幾個步驟的說明:

  1. 主隊列一路按順序執行任務——接着是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
  2. viewDidLoad 在主線程執行。
  3. 主線程目前在 viewDidLoad 內,正要到達 dispatch_sync 。
  4. dispatch_sync Block 被添加到一個全局隊列中,將在稍後執行。進程將在主線程掛起直到該 Block 完成。同時,全局隊列併發處理任務;要記得 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。
  5. 全局隊列處理 dispatch_sync Block 加入以前已經出如今隊列中的任務。
  6. 終於,輪到 dispatch_sync Block 。
  7. 這個 Block 完成,所以主線程上的任務能夠恢復。
  8. viewDidLoad 方法完成,主隊列繼續處理其餘任務。

dispatch_sync 添加任務到一個隊列並等待直到任務完成。dispatch_async 作相似的事情,但不一樣之處是它不會等待任務的完成,而是當即繼續「調用線程」的其它任務。

dispatch_async 回顧

- (void)viewDidLoad
{
  [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }

dispatch_async_in_action

  1. 主隊列一路按順序執行任務——接着是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
  2. viewDidLoad 在主線程執行。
  3. 主線程目前在 viewDidLoad 內,正要到達 dispatch_async 。
  4. dispatch_async Block 被添加到一個全局隊列中,將在稍後執行。
  5. viewDidLoad 在添加 dispatch_async 到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列併發地處理它未完成地任務。記住 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。
  6. 添加到 dispatch_async 的代碼塊開始執行。
  7. dispatch_async Block 完成,兩個 NSLog 語句將它們的輸出放在控制檯上。

在這個特定的實例中,第二個 NSLog 語句執行,跟着是第一個 NSLog 語句。並不老是這樣——着取決於給定時刻硬件正在作的事情,並且你沒法控制或知曉哪一個語句會先執行。「第一個」 NSLog 在某些調用狀況下會第一個執行。

下一步怎麼走?

在本教程中,你學習瞭如何讓你的代碼線程安全,以及在執行 CPU 密集型任務時如何保持主線程的響應性。

你能夠下載 GooglyPuff 項目,它包含了目前全部本教程中編寫的實現。在本教程的第二部分,你將繼續改進這個項目。

若是你計劃優化你本身的應用,那你應該用 Instruments 中的 Time Profile 模版分析你的工做。對這個工具的使用超出了本教程的範圍,你能夠看看 如何使用Instruments 來獲得一個很好的概述。

同時請確保在真實設備上分析,而在模擬器上測試會對程序速度產生很是不許確的印象。

在教程的下一部分,你將更加深刻到 GCD 的 API 中,作一些更 Cool 的東西。

若是你有任何問題或評論,可自由地加入下方的討論!

 

歡迎來到GCD深刻理解系列教程的第二部分(也是最後一部分)。

在本系列的第一部分中,你已經學到超過你想像的關於併發、線程以及GCD 如何工做的知識。經過在初始化時利用 dispatch_once,你建立了一個線程安全的 PhotoManager 單例,並且你經過使用 dispatch_barrier_async 和 dispatch_sync 的組合使得對 Photos 數組的讀取和寫入都變得線程安全了。

除了上面這些,你還經過利用 dispatch_after 來延遲顯示提示信息,以及利用 dispatch_async 將 CPU 密集型任務從 ViewController 的初始化過程當中剝離出來異步執行,達到了加強應用的用戶體驗的目的。

若是你一直跟着第一部分的教程在寫代碼,那你能夠繼續你的工程。但若是你沒有完成第一部分的工做,或者不想重用你的工程,你能夠下載第一部分最終的代碼

那就讓咱們來更深刻地探索 GCD 吧!

糾正過早彈出的提示

你可能已經注意到當你嘗試用 Le Internet 選項來添加圖片時,一個 UIAlertView 會在圖片下載完成以前就彈出,以下如所示:

Premature Completion Block

問題的癥結在 PhotoManagers 的 downloadPhotoWithCompletionBlock: 裏,它目前的實現以下:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    __block NSError *error; 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; } Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } }]; [[PhotoManager sharedManager] addPhoto:photo]; } if (completionBlock) { completionBlock(error); } }

在方法的最後你調用了 completionBlock ——由於此時你假設全部的照片都已下載完成。但很不幸,此時並不能保證全部的下載都已完成。

Photo 類的實例方法用某個 URL 開始下載某個文件並當即返回,但此時下載並未完成。換句話說,當 downloadPhotoWithCompletionBlock: 在其末尾調用 completionBlock 時,它就假設了它本身所使用的方法全都是同步的,並且每一個方法都完成了它們的工做。

然而,-[Photo initWithURL:withCompletionBlock:] 是異步執行的,會當即返回——因此這種方式行不通。

所以,只有在全部的圖像下載任務都調用了它們本身的 Completion Block 以後,downloadPhotoWithCompletionBlock: 才能調用它本身的 completionBlock 。問題是:你該如何監控併發的異步事件?你不知道它們什麼時候完成,並且它們完成的順序徹底是不肯定的。

或許你能夠寫一些比較 Hacky 的代碼,用多個布爾值來記錄每一個下載的完成狀況,但這樣作就缺失了擴展性,並且說實話,代碼會很難看。

幸運的是, 解決這種對多個異步任務的完成進行監控的問題,剛好就是設計 dispatch_group 的目的。

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 :

  • 自定義串行隊列:它很適合當一組任務完成時發出通知。
  • 主隊列(串行):它也很適合這樣的狀況。但若是你要同步地等待全部工做地完成,那你就不該該使用它,由於你不能阻塞主線程。然而,異步模型是一個頗有吸引力的能用於在幾個較長任務(例如網絡調用)完成後更新 UI 的方式。
  • 併發隊列:它也很適合 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 的隊列,此處,主隊列就是你所須要的。

對於這個特定的工做,上面的處理明顯更清晰,並且也不會阻塞任何線程。

太多併發帶來的風險

既然你的工具箱裏有了這些新工具,你大概作任何事情都想使用它們,對吧?

Thread_All_The_Code_Meme

看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已經注意到這裏的 for 循環,它迭代三次,下載三個不一樣的圖片。你的任務是嘗試讓 for 循環併發運行,以提升其速度。

dispatch_apply 恰好可用於這個任務。

dispatch_apply 表現得就像一個 for 循環,但它能併發地執行不一樣的迭代。這個函數是同步的,因此和普通的 for 循環同樣,它只會在全部工做都完成後纔會返回。

當在 Block 內計算任何給定數量的工做的最佳迭代數量時,必需要當心,由於過多的迭代和每一個迭代只有少許的工做會致使大量開銷以至它能抵消任何因併發帶來的收益。而被稱爲跨越式(striding)的技術能夠在此幫到你,即經過在每一個迭代裏多作幾個不一樣的工做。

譯者注:大概就能減小併發數量吧,做者是提醒你們注意併發的開銷,記在內心!

那什麼時候才適合用 dispatch_apply 呢?

  • 自定義串行隊列:串行隊列會徹底抵消 dispatch_apply 的功能;你還不如直接使用普通的 for 循環。
  • 主隊列(串行):與上面同樣,在串行隊列上不適合使用 dispatch_apply 。仍是用普通的 for 循環吧。
  • 併發隊列:對於併發循環來講是很好選擇,特別是當你須要追蹤任務的進度時。

回到 downloadPhotosWithCompletionBlock: 並用下列實現替換它:

- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
    __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t 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); Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); }]; [[PhotoManager sharedManager] addPhoto:photo]; }); dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ if (completionBlock) { completionBlock(error); } }); }

你的循環如今是並行運行的了;在上面的代碼中,在調用 dispatch_apply 時,你用第一次參數指明瞭迭代的次數,用第二個參數指定了任務運行的隊列,而第三個參數是一個 Block。

要知道雖然你有代碼保證添加相片時線程安全,但圖片的順序卻可能不一樣,這取決於線程完成的順序。

編譯並運行,而後從 「Le Internet」 添加一些照片。注意到區別了嗎?

在真機上運行新代碼會稍微更快的獲得結果。但咱們所作的這些提速工做真的值得嗎?

實際上,在這個例子裏並不值得。下面是緣由:

  • 你建立並行運行線程而付出的開銷,極可能比直接使用  for 循環要多。若你要以合適的步長迭代很是大的集合,那才應該考慮使用 dispatch_apply
  • 你用於建立應用的時間是有限的——除非實在太糟糕不然不要浪費時間去提早優化代碼。若是你要優化什麼,那去優化那些明顯值得你付出時間的部分。你能夠經過在 Instruments 裏分析你的應用,找出最長運行時間的方法。看看 如何在 Xcode 中使用 Instruments 能夠學到更多相關知識。
  • 一般狀況下,優化代碼會讓你的代碼更加複雜,不利於你本身和其餘開發者閱讀。請確保添加的複雜性能換來足夠多的好處。

記住,不要在優化上太瘋狂。你只會讓你本身和後來者更難以讀懂你的代碼。

GCD 的其餘趣味

等一下!還有更多!有一些額外的函數在不一樣的道路上走得更遠。雖然你不會太頻繁地使用這些工具,但在對的狀況下,它們能夠提供極大的幫助。

阻塞——正確的方式

這可能聽起來像是個瘋狂的想法,但你知道 Xcode 已有了測試功能嗎?:] 我知道,雖然有時候我喜歡僞裝它不存在,但在代碼裏構建複雜關係時編寫和運行測試很是重要。

Xcode 裏的測試在 XCTestCase 的子類上執行,並運行任何方法簽名以 test 開頭的方法。測試在主線程運行,因此你能夠假設全部測試都是串行發生的。

當一個給定的測試方法運行完成,XCTest 方法將考慮此測試已結束,並進入下一個測試。這意味着任何來自前一個測試的異步代碼會在下一個測試運行時繼續運行。

網絡代碼一般是異步的,所以你不能在執行網絡獲取時阻塞主線程。也就是說,整個測試會在測試方法完成以後結束,這會讓對網絡代碼的測試變得很困難。也就是,除非你在測試方法內部阻塞主線程直到網絡代碼完成。

注意:有一些人會說,這種類型的測試不屬於集成測試的首選集(Preferred Set)。一些人會贊同,一些人不會。但若是你想作,那就去作。

Gandalf_Semaphore

導航到 GooglyPuffTests.m 並查看 downloadImageURLWithString:,以下:

- (void)downloadImageURLWithString:(NSString *)URLString { NSURL *url = [NSURL URLWithString:URLString]; __block BOOL isFinishedDownloading = NO; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } isFinishedDownloading = YES; }]; while (!isFinishedDownloading) {} }

這是一種測試異步網絡代碼的幼稚方式。 While 循環在函數的最後一直等待,直到 isFinishedDownloading 布爾值變成 True,它只會在 Completion Block 裏發生。讓咱們看看這樣作有什麼影響。

經過在 Xcode 中點擊 Product / Test 運行你的測試,若是你使用默認的鍵綁定,也可使用快捷鍵 ⌘+U 來運行你的測試。

在測試運行時,注意 Xcode debug 導航欄裏的 CPU 使用率。這個設計不當的實現就是一個基本的 自旋鎖 。它很不實用,由於你在 While 循環裏浪費了珍貴的 CPU 週期;並且它也幾乎沒有擴展性。

譯者注:所謂自旋鎖,就是某個線程一直搶佔着 CPU 不斷檢查以等到它須要的狀況出現。由於現代操做系統都是能夠併發運行多個線程的,因此它所等待的那個線程也有機會被調度執行,這樣它所須要的狀況遲早會出現。

你可能須要使用前面提到的 Network Link Conditioner ,已便清楚地看到這個問題。若是你的網絡太快,那麼自旋只會在很短的時間裏發生,難以觀察。

譯者注:做者反覆提到網速太快,而咱們還須要對付 GFW,簡直淚流滿面!

你須要一個更優雅、可擴展的解決方案來阻塞線程直到資源可用。歡迎來到信號量。

信號量

信號量是一種老式的線程概念,由很是謙卑的 Edsger W. Dijkstra 介紹給世界。信號量之因此比較複雜是由於它創建在操做系統的複雜性之上。

若是你想學到更多關於信號量的知識,看看這個連接它更細緻地討論了信號量理論。若是你是學術型,那能夠看一個軟件開發中經典的哲學家進餐問題,它須要使用信號量來解決。

信號量讓你控制多個消費者對有限數量資源的訪問。舉例來講,若是你建立了一個有着兩個資源的信號量,那同時最多隻能有兩個線程能夠訪問臨界區。其餘想使用資源的線程必須在一個…你猜到了嗎?…FIFO隊列裏等待。

讓咱們來使用信號量吧!

打開 GooglyPuffTests.m 並用下列實現替換 downloadImageURLWithString:

- (void)downloadImageURLWithString:(NSString *)URLString { // 1 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSURL *url = [NSURL URLWithString:URLString]; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } // 2 dispatch_semaphore_signal(semaphore); }]; // 3 dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds); if (dispatch_semaphore_wait(semaphore, timeoutTime)) { XCTFail(@"%@ timed out", URLString); } }

下面來講明你代碼中的信號量是如何工做的:

  1. 建立一個信號量。參數指定信號量的起始值。這個數字是你能夠訪問的信號量,不須要有人先去增長它的數量。(注意到增長信號量也被叫作發射信號量)。譯者注:這裏初始化爲0,也就是說,有人想使用信號量必然會被阻塞,直到有人增長信號量。
  2. 在 Completion Block 裏你告訴信號量你再也不須要資源了。這就會增長信號量的計數並告知其餘想使用此資源的線程。
  3. 這會在超時以前等待信號量。這個調用阻塞了當前線程直到信號量被髮射。這個函數的一個非零返回值表示到達超時了。在這個例子裏,測試將會失敗由於它覺得網絡請求不會超過 10 秒鐘就會返回——一個平衡點!

再次運行測試。只要你有一個正常工做的網絡鏈接,這個測試就會立刻成功。請特別注意 CPU 的使用率,與以前使用自旋鎖的實現做個對比。

關閉你的網絡連接再運行測試;若是你在真機上運行,就打開飛行模式。若是你的在模擬器裏運行,你能夠直接斷開 Mac 的網絡連接。測試會在 10 秒後失敗。這很棒,它真的能按照預想的那樣工做!

還有一些瑣碎的測試,但若是你與一個服務器組協同工做,那麼這些基本的測試可以防止其餘人就最新的網絡問題對你說三道四。

使用 Dispatch Source

GCD 的一個特別有趣的特性是 Dispatch Source,它基本上就是一個低級函數的 grab-bag ,能幫助你去響應或監測 Unix 信號、文件描述符、Mach 端口、VFS 節點,以及其它晦澀的東西。全部這些都超出了本教程討論的範圍,但你能夠經過實現一個 Dispatch Source 對象並以一個至關奇特的方式來使用它來品嚐那些晦澀的東西。

第一次使用 Dispatch Source 可能會迷失在如何使用一個源,因此你須要知曉的第一件事是 dispatch_source_create 如何工做。下面是建立一個源的函數原型:

dispatch_source_t dispatch_source_create( dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue);

第一個參數是 dispatch_source_type_t 。這是最重要的參數,由於它決定了 handle 和 mask 參數將會是什麼。你能夠查看 Xcode 文檔 獲得哪些選項可用於每一個 dispatch_source_type_t 參數。

下面你將監控 DISPATCH_SOURCE_TYPE_SIGNAL 。如文檔所顯示的

一個監控當前進程信號的 Dispatch Source。 handle 是信號編號,mask 未使用(傳 0 便可)。

這些 Unix 信號組成的列表可在頭文件  signal.h 中找到。在其頂部有一堆 #define 語句。你將監控此信號列表中的 SIGSTOP信號。這個信號將會在進程接收到一個沒法迴避的暫停指令時被髮出。在你用 LLDB 調試器調試應用時你使用的也是這個信號。

去往 PhotoCollectionViewController.m 並添加以下代碼到 viewDidLoad 的頂部,就在 [super viewDidLoad] 下面:

- (void)viewDidLoad
{
  [super viewDidLoad]; // 1 #if DEBUG // 2 dispatch_queue_t queue = dispatch_get_main_queue(); // 3 static dispatch_source_t source = nil; // 4 __typeof(self) __weak weakSelf = self; // 5 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 6 source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue); // 7 if (source) { // 8 dispatch_source_set_event_handler(source, ^{ // 9 NSLog(@"Hi, I am: %@", weakSelf); }); dispatch_resume(source); // 10 } }); #endif // The other stuff }

這些代碼有點兒複雜,因此跟着註釋一步步走,看看到底發生了什麼:

  1. 最好是在 DEBUG 模式下編譯這些代碼,由於這會給「有關方面(Interested Parties)」不少關於你應用的洞察。 :] 
  2. Just to mix things up,你建立了一個 dispatch_queue_t 實例變量而不是在參數上直接使用函數。當代碼變長,分拆有助於可讀性。
  3. 你須要 source 在方法範圍以外也可被訪問,因此你使用了一個 static 變量。
  4. 使用 weakSelf 以確保不會出現保留環(Retain Cycle)。這對 PhotoCollectionViewController 來講不是徹底必要的,由於它會在應用的整個生命期裏保持活躍。然而,若是你有任何其它會消失的類,這就能確保不會出現保留環而形成內存泄漏。
  5. 使用 dispatch_once 確保只會執行一次 Dispatch Source 的設置。
  6. 初始化 source 變量。你指明瞭你對信號監控感興趣並提供了 SIGSTOP 信號做爲第二個參數。進一步,你使用主隊列處理接收到的事件——很快你就好發現爲什麼要這樣作。
  7. 若是你提供的參數不合格,那麼 Dispatch Source 對象不會被建立。也就是說,在你開始在其上工做以前,你須要確保已有了一個有效的 Dispatch Source 。
  8. 當你收到你所監控的信號時,dispatch_source_set_event_handler 就會執行。以後你能夠在其 Block 裏設置合適的邏輯處理器(Logic Handler)。
  9. 一個基本的 NSLog 語句,它將對象打印到控制檯。
  10. 默認的,全部源都初始爲暫停狀態。若是你要開始監控事件,你必須告訴源對象恢復活躍狀態。

編譯並運行應用;在調試器裏暫停並當即恢復應用,查看控制檯,你會看到這個來自黑暗藝術的函數確實能夠工做。你看到的大概以下:

2014-03-29 17:41:30.610 GooglyPuff[8181:60b] Hi, I am:

你的應用如今具備調試感知了!這真是超級棒,但在真實世界裏該如何使用它呢?

你能夠用它去調試一個對象並在任何你想恢復應用的時候顯示數據;你一樣能給你的應用加上自定義的安全邏輯以便在惡意攻擊者將一個調試器鏈接到你的應用上時保護它本身(或用戶的數據)。

譯者注:好像挺有用!

一個有趣的主意是,使用此方式的做爲一個堆棧追蹤工具去找到你想在調試器裏操縱的對象。

What_Meme

稍微想一想這個狀況。當你意外地中止調試器,你幾乎歷來都不會在所需的棧幀上。如今你能夠在任什麼時候候中止調試器並在你所需的地方執行代碼。若是你想在你的應用的某一點執行的代碼很是難以從調試器訪問的話,這會很是有用。有機會試試吧!

I_See_What_You_Did_Meme

將一個斷點放在你剛添加在 viewDidLoad 裏的事件處理器的 NSLog 語句上。在調試器裏暫停,而後再次開始;應用會到達你添加的斷點。如今你深刻到你的 PhotoCollectionViewController 方法深處。你能夠訪問 PhotoCollectionViewController 的實例獲得你關心的內容。很是方便!

注意:若是你尚未注意到在調試器裏的是哪一個線程,那如今就看看它們。主線程老是第一個被 libdispatch 跟隨,它是 GCD 的座標,做爲第二個線程。以後,線程計數和剩餘線程取決於硬件在應用到達斷點時正在作的事情。

在調試器裏,鍵入命令:po [[weakSelf navigationItem] setPrompt:@"WOOT!"]

而後恢復應用的執行。你會看到以下內容:

Dispatch_Sources_Xcode_Breakpoint_Console

Dispatch_Sources_Debugger_Updating_UI

使用這個方法,你能夠更新 UI、查詢類的屬性,甚至是執行方法——全部這一切都不須要重啓應用併到達某個特定的工做狀態。至關優美吧!

譯者注:發揮這一點,是能夠作出一些調試庫的吧?

以後又該往何處去?

你能夠在此下載最終的項目

我討厭再次說起此主題,但你真的要看看 如何使用 Instruments 教程。若是你計劃優化你的應用,那你必定要學會使用它。請注意 Instruments 擅長於分析相對執行:比較哪些區域的代碼相對於其它區域的代碼花費了更長的時間。若是你嘗試計算出某個方法實際的執行時間,那你可能須要拿出更多的自釀的解決方案(Home-brewed Solution)。

一樣請看看 如何使用 NSOperations 和 NSOperationQueues 吧,它們是創建在 GCD 之上的併發技術。大致來講,若是你在寫簡單的用過就忘的任務,那它們就是使用 GCD 的最佳實踐,。NSOperations 提供更好的控制、處理大量併發操做的實現,以及一個以速度爲代價的更加面向對象的範例。

記住,除非你有特別的緣由要往下流走(譯者的玩笑:即便用低級別 API),不然永遠應嘗試並堅持使用高級的 API。若是你想學到更多或想作某些很是很是「有趣」的事情,那你就應該冒險進入 Apple 的黑暗藝術。

祝你好運,玩得開心!有任何問題或反饋請在下方的討論區貼出!

相關文章
相關標籤/搜索