天天看點

深入學習中央排程(GCD)--第一部分

原文位址:
   https://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1​ 
  
  
        更新說明:查閱我們基于iOS8.0和Swift下中央排程(
   https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1)教程這塊的更新版本。
  
 盡管中央調用(簡稱GCD)已經存在一段時間了,但并不是每個人都知道如何有效地使用它。這是可以了解的,并發本身就是棘手的,然而基于C語言的GCD API看起來像一套深入OC世界的彎角(轉換器)。這個系列教程分兩部分,深入地介紹中央排程(GCD)。

 在這兩部分中,第一部分解釋了什麼是GCD以及GCD常用的幾個基本函數,在第二部分中,将會介紹幾個GCD提供的更進階的功能。

 **
      

什麼是GCD?

** 

libdispatch俗稱GCD,蘋果提供的庫,用以支援在iOS和OS X的多核硬體上執行并行代碼。它有以下幾個有點: 

1、GCD可以通過延緩耗時的計算任務放在背景運作來提高App的響應能力 

2、GCD提供了比加鎖和線程更加簡單的并發模型來避免并發bugs 

3、GCD可以使用高性能的執行單元優化代碼,比如常用的模式:單例 

本教程假設你已經對blocks和GCD有一個基本的了解,如果是全新接觸GCD,可以查閱供初學者;了解學習要點的基于iOS的多線程處理和中央排程。

**
           

GCD術語

** 

要了解GCD,需要能應付自如幾個跟線程和并發相關的概念。這些可能既模糊又微妙,是以在GCD的上下文中花點時間去簡要回顧一下它們。 

1、串行與并行 

這倆術語描述了任務被執行時彼此的關系。串行執行任務每次執行一個任務,并發執行任務可能在同一時間執行多個任務。 

盡管這些術語有廣泛的應用,但對于該教程來說,你可以把一個任務當做是一個OC代碼塊。不知道什麼是塊(block)?請查閱在iOS5下如何使用blocks。實際上,你也可以以函數指針的方式使用GCD,但在大多數情況下這樣使用起來更加棘手。Blocks是更加簡單的。 

2、同步與異步 

在GCD下,這倆術語描述了當一個功能完成之後與之關聯的另一個任務功能如何請求GCD調用執行。同步意味着僅當任務按序執行完畢之後才會傳回。 

異步,換句話說,就是立即傳回預定的任務要執行但是不會等待。是以,異步不會阻塞目前線程的執行繼續向下執行。 

注意,當你看到一個阻塞目前線程、函數或操作的的同步操作時,不要弄混了。這個動作塊描述了一個功能是如何影響它的線程,并且沒有連接配接到名詞塊(描述了一個在OC中的字面匿名函數且定義了一個送出到GCD的任務)。 

3、臨界區 

這是一段不能被并發執行的代碼,那就是,同時隻能有一個線程執行。這就是一般并行程序通路共享資源(比如變量)的代碼變壞的原因。 

4、争用條件 

這種情況是由軟體系統依賴一個特殊的序列或在一個不受控制的事件(如:程式的并發任務的确切執行順序)執行時間下産生的。争用情況可能産生不可預期的行為,而且不是立即可以通過代碼檢查就能發現的。 

5、死鎖 

在大多數情況下,兩個(有時更多)元素被說成是線程死鎖是因為他們陷入了彼此等待而不能正常的完成或執行其他行為。一個不能結束是因為正在等待另一個結束。另一個不能完成是以為在等待第一個結束。 

6、線程安全 

線程安全的代碼可以安全地被多個線程或并發任務調用而不會引起任何問題(如:資料異常、崩潰等)。線程不安全的代碼同一時間下僅僅可以在一個上下文中運作。一個線程安全的例子就是不可變字典,你可以在多個線程中同時使用而不會出問題。換句話說可變字典不是線程安全的,因為同一時間下,僅可以可以在一個線程中通路(安全而不出問題)。 

7、上下文切換 

上下文切換是指當一個程式存儲和恢複執行狀态(當你在單個程序中在不同線程間切換時)。這中程式在你寫多任務程式時很常見,但是也帶來了一些額外的開銷作為代價。 

并發以并行 

并發和并行常常被同時提到,是以簡要的說明下兩者之間的差別還是值得的。 

分離的并發代碼可以被“同時”執行。然而,這是由系統決定如何發生-或者如果完全發生的話。多核心得裝置同時執行多個線程通過并行。然而在單核心裝置中為了達到并發,運作一個線程時必須通過上下文切花來運作另一個線程。這通常發生的足夠快,我們可以假想它按下圖方式執行:

深入學習中央排程(GCD)--第一部分

盡管你可能會在GCD下寫代碼以使用并發執行,但最終是由GCD決定多少并行是必須的。并行必定并發,但是并發不能保證并行。 

這裡更深層點的問題是,并發實際上是結構上的。當你在頭腦中構思GCD代碼時,就要規劃代碼結構以拆分為可同時運作的工作片和可以不必同時運作的任務。如果想更深入地研究這個問題,查閱這個精彩的演講(this excellent talk by Rob Pike.)。 

隊列 

GCD提供了排程隊列以處理代碼塊,這些隊列管理你送出到GCD的任務并按FIFO順序執行。這保證了第一個進入隊列的任務是第一個開始執行的,第二個添加到隊列的将第二被執行,接下來按序。 

所有的排程隊列對他們自己而言是線程安全的,可以在不同的線程中同時通路。GCD的優勢是明顯的(當你了解自己不同部分的代碼是如何線程安全地通路排程隊列時)。關鍵就是選擇合适的排程隊列類型和合适的dispatching函數送出自己的任務到隊列。 

這節中,将看到兩種類型的排程隊列,GCD提供的特定隊列以及通過一些列子說明如何使用GCD排程函數添加任務到排程隊列。 

1、串行隊列 

在串行隊列中的任務一次執行一個,每個任務的開始必須是前面的任務完成之後。當然,也無需知道一個代碼段何時結束及下一個何時開始,如圖所示: 

深入學習中央排程(GCD)--第一部分

這些任務的執行時間是由GCD控制的,能知道的就是一次執行一個任務,按照添加到隊列的順序按序執行。 

由于在串行隊列中不會有兩個任務并發執行,也就沒有同時通路臨界區的并發問題,以保護臨界區不會被争競條件影響。是以,通路臨界區的唯一方式就是通過送出到排程隊列的任務通路,保證臨界區安全。 

2、并發隊列 

在并發隊列中的任務僅僅能保證按照添加進的順序啟動,and這也是能保證的所有。元素可能一任何順序結束,你也不能确定下一個block還要多長時間才能開始,同時在執行的blocks數目也不能确定,這都是GCD決定的。 

下面的圖表展示了一個任務執行的示例,其中GCD控制了4個并發任務: 

深入學習中央排程(GCD)--第一部分

備注:現在block1,2和3運作很快,一個接一個。block1開始執行花費了一點時間在block差不多執行結束後才開始。同樣的,在block2開始後block3也開始執行了但并不是block2結束後才開始。 

何時開始一個block執行完全由GCD決定。如果執行一個block的時間逾時了,GCD會決定是否在另一個可用的核心上開始另一個任務或者切換上下文去執行另一個不同的 代碼塊。 

令人欣喜的是,GCD提供了至少5個特别的隊列類型可供選擇。 

3、隊列類型 

首先,系統提供了一個特别的串行隊列成為主隊列。像任何串行隊列一樣,在這個隊列中一次隻能執行一個任務。然而,它可以保證所有的任務都在主線程(必須要保證所有更新UI的操作必須在這個線程執行)中執行。這個隊列是一個用于接收UIView消息和通知的隊列。 

該系統還提供了其他幾個并發隊列。這些統稱為全局排程隊列。有4個不同優先級的全局隊列:background, low, default, high.值得一提的是,蘋果的api也使用這些隊列,是以你添加任何任務到這些隊列,其中任務不隻有你添加的。 

此外,你也可以建立你自定義的串行或并行隊列。這意味着至少有5個隊列任由你處置:主隊列,4個全局隊列,再加上任何一個你添加的自定義的隊列。 

這就是排程隊列的“偉大藍圖”。 

GCD的藝術來源于選擇合适的隊列去送出任務。最好的經驗就是通過下面的例子學習,在哪裡我們根據長期經驗提供了一些一般性的建議。

開始

由于本教程的目的是既要簡單又要安全的從不同的線程調用代碼,你将從頭到尾完成這個GoodPuff項目。
      GoodPuff是一個非優化的,非線程安全的app。在這裡你要瞪大眼睛去分辨COre image API的使用。對于基本的圖檔來說,你可以從相冊庫中選擇也可以從一系列未知的圖檔url下載下傳使用 。
      Download the project here.
      一旦下載下傳好,提取到一個合适的位置,用Xcode打開并運作它,看起來會像下面一樣:
           
深入學習中央排程(GCD)--第一部分

注意:當你選擇下載下傳圖檔選項時,UIA了人VIew會過早的彈出,浙江在本系列的第二部分修複。 

在這個項目中使用了四個類: 

PhotoCollectionViewController:這是啟動app後的第一個視圖控制器。它以縮略圖的形式展示所有選中的圖檔。 

PhotoDetailViewController:這個以大圖的形式在UIScrollView中展示圖檔 

Photo:這是一個類聚合,其可以從NSURL或ALAsset建立圖檔。該類提供圖檔,縮略圖或一個下載下傳圖檔的狀态。 

PhotoManager:該類管理所欲照片執行個體。

用dispatch_sync處理背景任務

回過頭來看該app,從相冊庫添加一些圖檔或使用網絡下載下傳一些。 

關注下在點選UICollectionViewCell後,花費了多長時間去執行個體化顯示一個新的PhotoDetailViewController,有明顯的滞後,尤其是在反應慢的裝置上預覽大圖時。 

在很多複雜的環境下,執行UIViewController’s viewDidLoad很容易過載,在新視圖顯示之前常常要等待較長時間,在加載時不是必要的工作可以放在背景處理。 

這聽起來像是異步工作。 

打開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, ), ^{ // 1
        UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
        dispatch_async(dispatch_get_main_queue(), ^{ // 2
            [self fadeInNewImage:overlayImage]; // 3
        });
    });
}
           

上面是将要修改的代碼。 

1、首先從把任務從主線程放到全局隊列中。因為這是dispatch_async(異步),代碼塊被異步送出意味着将在從線程中調用。這可以讓viewDidLoad可以盡快在主線程中執行完,讓加載感覺特别快。同時,圖檔加載開始執行,将在之後某個時間完成。 

2、這時候,圖檔加載處理已完成,你已經生成一個新的圖檔。你可以拿新圖檔去更新顯示。到主隊列中添加一個新的工作。記住,隻能在主線程中更新UI。 

3、最後,在fadeInNewImage中更新UI。 

生成并運作app,選擇圖檔你會發現試圖控制加載明顯更快,并在短暫的時間後顯示大圖。這提供了一個不錯的檢視大圖的效果。 

同樣的,如果你試着加載一個出奇巨大的圖檔時,這個app也不會再加載視圖控制器的時候卡住,同時app可以很好的擴充。 

正如上文提到的,dispatch_async将添加block到一個隊列并立即傳回。該任務将在一段時間之後被GCD決定執行。當需要執行一個網絡操作或cpu耗時的任務時放在背景不會阻塞目前線程的執行。 

下面是一個如何、何時使用dispatch_async的各種隊列類型的快速向導: 

自定義串行隊列:當要背景串行執行任務、要跟蹤它時,這是一個不錯的選擇。這消除了資源争用,因為你已經知道同一時間隻能有一個任務執行。注意,如果你需要從一個方法擷取資料,必須内嵌另一個 block進去同時考慮采用dispatch_sync方式。 

主隊列(串行):在一個并行的隊列中完成任務後去更新UI,選擇它。這樣做的話,将内嵌另一個block到block中。同理,如果在主隊列中調用dispatch_async,隻能保證新任務在目前方法結束後一段時間内将會執行。 

并行隊列:要在背景執行非UI工作可以選擇它。

延時工作dispatch_after 

考慮一下app的使用者體驗,當使用者第一次打開app時可能會很困惑,不知道要做什麼。 

展示一個提示資訊可能是一個不錯的主意,當在PhotoManager中沒有任何照片時。但是,你也需要考慮使用者的眼睛是如何浏覽螢幕首頁的,如果你展示圖示資訊太快(一閃而過)的話,他們可能根本沒有看清視圖中顯示的内容。在顯示提示資訊時加上1~2秒的延時足夠吸引使用者注意了。 

添加下面的代碼再執行去試着實作顯示延時:(showOrHideNavPromote PhotoCollectionViewController)

- (void)showOrHideNavPrompt
{
    NSUInteger count = [[PhotoManager sharedManager] photos].count;
    double delayInSeconds = ;
    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];
        }
    });
}
           

生成并運作app。輕微的延遲,将吸引使用者的注意,提示他們該怎麼做。

dispathc_after工作就像一個延時的dispatch_async。你依然沒有實際執行時間的控制權,但是可以在其傳回之前取消。 

想知道什麼時候使用dispatch_after嗎? 

1、自定義串行的隊列:在自定義串行隊列上小心使用,最好在注隊列使用。 

2、主隊列:主隊列可以使用dispatch_after,Xcode有一個nice模闆去自動建立使用它。 

3、并發隊列:在自定義的并發隊列上使用dispatch_after時要小心,而且很少用。堅持在主隊列使用它。

使單例模式線程安全

單例模式:既愛又恨,在iOS和在伺服器器系統上的web一樣受歡迎。 

複雜的單例關系常常不是線程安全的。這種關系要合理的使用:單例模式就是常常多個視圖控制器同時通路單個單一執行個體。 

對單例來說,線程關系涉及到初始化、讀取和寫入資訊。 

PhotoManager類就是單例類,在目前狀态下就面臨這些問題。為了更快的看到問題所在,将要在單例中建立一個受控的争用條件。 

定位到PhotoManager.m 然後找到sharedManger,代碼想下面這樣:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
           
目前狀态下代碼是很簡單的,你建立了一個單例然後初始化了一個私有數組(photoArray)。
但是,if條件分支不是線程安全的,如果你多次調用它,很有可能出現線上程A中進入if代碼段然後在執行sharedManager配置設定之前進行了上下文切換。然後線上程B中可能也進入if條件,配置設定了一個單執行個體而後退出。當系統上下文切換回線程A後,繼續配置設定領一個單執行個體後退出。同時将産生兩個單執行個體,這不是我們想看到的。
 為了防止這種情況發生,替換sharedmanager方法用下面的實作:
           
+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        [NSThread sleepForTimeInterval:];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
           
上面代碼中,線上程休眠方法中強制進行上下文切換。打開AppDelegate.m添加如下代碼:
           
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, ), ^{
    [PhotoManager sharedManager];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, ), ^{
    [PhotoManager sharedManager];
});
           
這将建立多個異步并發調用來執行個體化單例并會出現上文所述的争用情況。
 生成并運作項目,檢查控制台的輸出,将會看到多個單執行個體初始化,如下所示:
           
深入學習中央排程(GCD)--第一部分

注意:有幾行顯示了單例執行個體不同的位址,偏離了單例的初衷,不是嗎? 

輸出顯示隻應被執行一次的臨界區卻被執行了多次。誠然,現在是你強制這種情況發生,但是你可以想象一下這種情況也會在不經意間偶然出現。 

注:基于系統之上的其他事件很難控制,一系列的NSLog列印證明這點。線程問題很難跟蹤因為它很難複現。 

為了糾正這種問題,當運作在臨界區的if條件中時,初始化代碼應該僅被執行一次并阻塞其他執行個體。這個正是dispatch_once做的事情。 

替換單例中中的if條件語句用下面的單例初始化實作:

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

生成并運作app,查閱控制台輸出,你講看到僅有一個單執行個體被初始化,這才是我們想要的單例模式。

現在既然了解了防止争用條件的重要性,就删除AppDelegate.m中添加的diapatch_async語句然後替換單裡初始化的實作用下面的實作:

+ (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,當一個線程已經在代碼塊中是将獨占臨界區知道完成。
           
深入學習中央排程(GCD)--第一部分

應該指出的是這僅僅是共享執行個體的線程安全,并不一定類線程安全。你也可以有其他的臨界區,例如:然和可操作的内部資料。這些需要使用線程的安全的其他方式,譬如:同步黨文資料,下面将會看到。

處理讀和寫的問題

線程安全的執行個體化單例不是唯一的問題。如果單例的屬性是一個可變的對象,你就需要考慮對象本身是否是線程安全的。 

Foundation中的基礎容器是線程安全的嗎?答案是-不是的。蘋果維護了一系列有益的非線程安全的基礎資料類型。 

盡管,很多線程可以讀取一個可變的數組而沒有問題,但讓一個線程去修改數組在其他線程讀取的時候是不安全的。你的單例模式沒有避免這種情況發生。 

為了看到問題,看下addPhoto在PhotoManager.m中(已經被替換為如下:)

- (void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}
           
這是一個寫操作去修改一個可變的數組。
 現在修改photos屬性如下:
           
- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}
           
這個屬性的getter方法是一個讀方法去通路這個可變數組。這個調用獲得一份不可變的拷貝以免被不當破壞,但是沒有提供任何保護(當一個線程正在寫方法addPhoto時,另外線程去讀這個屬性)。
 這就是軟體系統的讀寫問題。GCD提供了一個優雅的解決方案通過使用排程障礙來建立讀寫鎖。
 排程障礙(栅欄)是一組函數像在并行隊列中的串行式障礙一樣。使用GCD阻塞API確定送出的閉包是該特定隊列上在特定時間是唯一的被執行元素。這就意味着所有被送出到隊列的元素必須在閉包被執行之前完成。
 當輪到該閉包時,阻塞執行閉包確定在這段時間内隊列不會執行其他閉包。一旦完成,隊列傳回到預設實作位置。GCD提供同步和異步兩個障礙方法。
 下面的圖檔說明了障礙函數在各種異步任務中的影響:
           
深入學習中央排程(GCD)--第一部分

請注意如何讓正常的操作隊列行為就像一個正常的并發隊列。但是當障礙執行的時候,它本質上就是一個串行隊列。隻有該障礙在執行。在障礙完成之後,隊列回歸為正常的并發隊列。 

下面是何時會用,何時不會用: 

1、自定義串行隊列:在這裡選用很糟糕,因為一個串行隊列在任何時候都是僅有一個任務在執行。 

2、全局并發隊列:這裡注意,不建議選用,因為系統可能正在使用該隊列而你不能自己獨占他們自己一個人使用。 

3、自定義并發隊列:這是一個很好的選擇以原子操作的方式去通路臨界區。任何你正在設定或初始化的且需要線程安全的都可以選用。 

既然,唯一可以正當選用的選擇就是自定義并發隊列,你可以建立自己的處理在單獨的讀和寫函數中。并發隊列允許同時多個讀操作。 

打開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、這是對象添加到數組的實際代碼。因為這是一個障礙閉包,這個閉包将永遠不會和其他閉包同時執行在該隊列中。
      4、最後你發送了一個通知表明添加了一個圖檔。這個通知将從主線程發送因為它要處理UI工作。是以這裡排程了一個異步任務到主隊列去處理通知。
 注意寫操作,你也需要實作圖檔讀操作。
 為了確定寫入方的線程安全,你需要在該隊列中執行讀操作。你需要從函數中傳回,是以你不能異步排程執行因為那樣将導緻在讀函數傳回之前永遠不會執行。
 在這種情況下,同步将是一個很好的選擇。
 dispatch_sync同步送出任務然後等待直到完成才執行傳回。使用dispatch_sync來保持跟蹤你的dispatch_barrier工作,或在你可以通過閉包使用資料之前你需要等待操作完成。
 你需要小心。想想一下,如果你調用dispatch_sync然而作用目标即目前隊列已經在執行了。這将導緻死鎖因為調用将等待閉包完成,但是閉包(它甚至不能開始)将在目前正在執行的、不可能結束的閉包結束後才能結束。這将迫使你意識到你正在調用的隊列就是你正在傳遞的隊列。
 下面是一個快速的預覽何時何地可以使用dispatch_sync:
      1>、自定義串行隊列:這種情況下要非常小心,如果你正在運作的一個隊列正好是你dispatch_sync的目标隊列這将導緻死鎖。
     2>、 主隊列:要小心使用,原因跟上面一樣。這種情況也同樣存在潛在的死鎖。
      3>、并行隊列:這是一個不錯的選擇通過dispatch_barrier或者當等待一個任務完成以便你可以進行下一步操作時。
           

還在PhotoManager.m,用下面的實作替換屬性:

- (NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}
           
這是一個讀函數,依次看注釋就會發現:
     1、__block關鍵字允許在塊内部改變該對象,沒有這個的話,array在塊内将是隻讀的,你的代碼甚至不能通過編譯。
      2、隊列中的同步排程執行讀操作
      3、儲存photoArray的拷貝然後傳回。
           

祝賀你,你的PhotoManager單例現在是線程安全的。無論在哪兒或者以何種方式讀或寫圖檔,它都将以線程安全的方式毫無意外的正常工作。 

最後,你需要初始化你的并發隊列屬性。像下面這樣改變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建立一個并發的隊列。第一個參數是反向DNS域名。這樣的描述在調試的時候很有用。第二個參數辨別隊列是串行還是并行。
 注:當在web上查找例子時,你常常看到别人穿0或者NULL作為dispatch_queue_Create的第二個參數。這是一種已經過時的方式,使用具體的(系統提供的枚舉類型DISPATCH_QUEUE_CONCURRENT等)作為參數總歸是更好的。
 祝賀你,你的PhotoManager單例現在是線程安全的。無論在哪兒或者以何種方式讀或寫圖檔,它都将以線程安全的方式毫無意外的正常工作。
           

視圖形式檢視排隊

現在是不是還是不能完全掌握GCD的要點?确信可以使用GCD方法建立簡單的例子并且使用斷點和NSLog去了解正在發生的事情(GCD執行過程中)。
 下面提供了兩個GIFs的例子幫助你加強dispatch_async和dispatch_sync的了解。代碼以輔助可視化工具的形式包含在GIF圖中,注意左邊GIF中斷點的每一步以及右邊關聯隊列的狀态。
           

dispatch_sync初探:

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), )) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}
           
深入學習中央排程(GCD)--第一部分

下面簡要介紹下關系圖表: 

1、主隊列按序執行,接下來就是一個任務初始化(視圖控制器初始化加載viewDidLoad)。 

2、viewDidLoad在主線程執行。 

3、同步閉包被添加到全局隊列且稍後執行。程序上表現是主線程停止知道該閉包完成。同時,全局隊列是并發執行任務的,在全局隊列上是按FIFO的順序喚起閉包的執行,但是可能是并發執行的。全局隊列同時還處理在該同步閉包添加到隊列之前就已經存在的任務。 

4、最後,該同步閉包按序執行。 

5、該閉包完成之後,主線程才被喚醒。 

6、viewDidLoad方法執行完畢,主隊列接着處理其他任務。 

同步添加任務到隊列,然後等待直到該任務完成。異步添加的話,唯一不同的是在被調起的線程裡無需等待任務完成就可以繼續執行向下執行。

dispatch_async初探:
override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), )) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}
           
深入學習中央排程(GCD)--第一部分

1、主隊列按序執行,接下來就是初始化一個視圖控制器任務,viewDidLoad在主線程執行。 

2、viewDidLoad在主線程執行。 

3、主線程現在在viewDidLoad裡,剛好執行到dispatch_async。 

4、異步閉包被添加到全局隊列,稍後執行。 

5、viewDidLoad在異步閉包添加到全局隊列之後繼續執行,主線程繼續執行未完成的任務。同時,全局隊列并發的執行其未完成的任務。記住:全局隊列任務是按FIFO順序出棧執行,但是執行過程中可以是并發的。 

6、被添加的異步閉包正在執行。 

7、異步閉包完成,同時控制台已經有NSLog輸出。 

在該熱定情況下,第二個NSLog緊随第一個NSLog執行。但并非總是如此,這取決于正在執行的硬體時間,你沒有辦法知道那個輸出先執行。在一些調用中先執行的NSLog可能是第一個NSLog中執行的。

接下來學習什麼?

在這個教程中,已經了解了如何使代碼線程安全,以及在CPU多任務處理下如何保持主線程的響應能力。 

你可以下載下傳GooglyPuff Project 工程,其中包含了所有目前教程中的實作。第二部分的教程中你将去改進這個項目。 

如果你計劃優化你的app,你應該使用Instruments進行性能分析。使用這個工具超出了本教程的範圍,是以你應該查閱一些關于如何使用它的優秀文章。 

確定有真機可以使用,因為模拟器測試出來的記過和真實使用者使用的體驗回報是不一樣的。 

在下一部分(https://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2)的教程中,你将深入的連接配接GCD的API來做一些更酷的東西。 

如果有問題或者建議,可以自由的加入下面的讨論區。

***<第一次做翻譯工作,太難了。做的不好,歡迎各位大大們不吝批評指正。萬分感謝!>***
           
***<另外格式也不太好,弄完之後從印象筆記拷出來的,部落格的格式編輯有點難搞,就這樣将就着看吧。>***