本文翻譯自 www.raywenderlich.com/60749/grand…html
原做者:Derek Selanderios
譯者:@nixzhugit
==========================================github
雖然 GCD 已經出現過一段時間了,但不是每一個人都明瞭其主要內容。這是能夠理解的;併發一直很棘手,而 GCD 是基於 C 的 API ,它們就像一組尖銳的棱角戳進 Objective-C 的平滑世界。咱們將分兩個部分的教程來深刻學習 GCD 。vim
在這兩部分的系列中,第一個部分的將解釋 GCD 是作什麼的,並從許多基本的 GCD 函數中找出幾個來展現。在第二部分,你將學到幾個 GCD 提供的高級函數。數組
GCD 是 libdispatch
的市場名稱,而 libdispatch 做爲 Apple 的一個庫,爲併發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具備如下優勢:xcode
本教程假設你對 Block 和 GCD 有基礎瞭解。若是你對 GCD 徹底陌生,先看看 iOS 上的多線程和 GCD 入門教程 學習其要領。安全
要理解 GCD ,你要先熟悉與線程和併發相關的幾個概念。這二者均可能模糊和微妙,因此在開始 GCD 以前先簡要地回顧一下它們。網絡
這些術語描述當任務相對於其它任務被執行,任務串行執行就是每次只有一個任務被執行,任務併發執行就是在同一時間能夠有多個任務被執行。多線程
雖然這些術語被普遍使用,本教程中你能夠將任務設定爲一個 Objective-C 的 Block 。不明白什麼是 Block ?看看 iOS 5 教程中的如何使用 Block 。實際上,你也能夠在 GCD 上使用函數指針,但在大多數場景中,這實際上更難於使用。Block 就是更加容易些!
在 GCD 中,這些術語描述當一個函數相對於另外一個任務完成,此任務是該函數要求 GCD 執行的。一個_同步_函數只在完成了它預約的任務後才返回。
一個_異步_函數,恰好相反,會當即返回,預約的任務會完成但不會等它完成。所以,一個異步函數不會阻塞當前線程去執行下一個函數。
注意——當你讀到同步函數「阻塞(Block)」當前線程,或函數是一個「阻塞」函數或阻塞操做時,不要被搞糊塗了!動詞「阻塞」描述了函數如何影響它所在的線程而與名詞「代碼塊(Block)」沒有關係。代碼塊描述了用 Objective-C 編寫的一個匿名函數,它能定義一個任務並被提交到 GCD 。
譯者注:中文不會有這個問題,「阻塞」和「代碼塊」是兩個詞。
就是一段代碼不能被併發執行,也就是,兩個線程不能同時執行這段代碼。這很常見,由於代碼去操做一個共享資源,例如一個變量若能被併發進程訪問,那麼它極可能會變質(譯者注:它的值再也不可信)。
這種情況是指基於特定序列或時機的事件的軟件系統以不受控制的方式運行的行爲,例如程序的併發任務執行的確切順序。競態條件可致使沒法預測的行爲,而不能經過代碼檢查當即發現。
兩個(有時更多)東西——在大多數狀況下,是線程——所謂的死鎖是指它們都卡住了,並等待對方完成或執行其它操做。第一個不能完成是由於它在等待第二個的完成。但第二個也不能完成,由於它在等待第一個的完成。
線程安全的代碼能在多線程或併發任務中被安全的調用,而不會致使任何問題(數據損壞,崩潰,等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary
。你能夠在同一時間在多個線程中使用它而不會有問題。另外一方面,NSMutableDictionary
就不是線程安全的,應該保證一次只能有一個線程訪問它。
一個上下文切換指當你在單個進程裏切換執行不一樣的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很廣泛,但會帶來一些額外的開銷。
併發和並行一般被一塊兒提到,因此值得花些時間解釋它們之間的區別。
併發代碼的不一樣部分能夠「同步」執行。然而,該怎樣發生或是否發生都取決於系統。多核設備經過並行來同時執行多個線程;然而,爲了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,而後運行另外一個線程或進程。這一般發生地足夠快以至給咱們併發執行地錯覺,以下圖所示:
雖然你能夠編寫代碼在 GCD 下併發執行,但 GCD 會決定有多少並行的需求。並行_要求_併發,但併發並不能_保證_並行。
更深刻的觀點是併發其實是關於_構造_。當你在腦海中用 GCD 編寫代碼,你組織你的代碼來暴露能同時運行的多個工做片斷,以及不能同時運行的那些。若是你想深刻此主題,看看 這個由Rob Pike作的精彩的講座 。
GCD 提供有 dispatch queues
來處理代碼塊,這些隊列管理你提供給 GCD 的任務並用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列裏的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。
全部的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程並行的訪問它們。當你瞭解了調度隊列如何爲你本身代碼的不一樣部分提供線程安全後,GCD的優勢就是顯而易見的。關於這一點的關鍵是選擇正確_類型_的調度隊列和正確的_調度函數_來提交你的工做。
在本節你會看到兩種調度隊列,都是由 GCD 提供的,而後看一些描述如何用調度函數添加工做到隊列的例子。
串行隊列中的任務一次執行一個,每一個任務只在前一個任務完成時纔開始。並且,你不知道在一個 Block 結束和下一個開始之間的時間長度,以下圖所示:
這些任務的執行時機受到 GCD 的控制;惟一能確保的事情是 GCD 一次只執行一個任務,而且按照咱們添加到隊列的順序來執行。
因爲在串行隊列中不會有兩個任務併發運行,所以不會出現同時訪問臨界區的風險;相對於這些任務來講,這就從競態條件下保護了臨界區。因此若是訪問臨界區的惟一方式是經過提交到調度隊列的任務,那麼你就不須要擔憂臨界區的安全問題了。
在併發隊列中的任務能獲得的保證是它們會按照被添加的順序開始執行,但這就是所有的保證了。任務可能以任意順序完成,你不會知道什麼時候開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這徹底取決於 GCD 。
下圖展現了一個示例任務執行計劃,GCD 管理着四個併發任務:
注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始後,Block 1等待了好一下子纔開始。一樣, Block 3 在 Block 2 以後纔開始,但它先於 Block 2 完成。
什麼時候開始一個 Block 徹底取決於 GCD 。若是一個 Block 的執行時間與另外一個重疊,也是由 GCD 來決定是否將其運行在另外一個不一樣的核心上,若是那個核心可用,不然就用上下文切換的方式來執行不一樣的 Block 。
有趣的是, GCD 提供給你至少五個特定的隊列,可根據隊列類型選擇使用。
首先,系統提供給你一個叫作 主隊列(main queue)
的特殊隊列。和其它串行隊列同樣,這個隊列中的任務一次只能執行一個。然而,它能保證全部的任務都在主線程執行,而主線程是惟一可用於更新 UI 的線程。這個隊列就是用於發生消息給 UIView
或發送通知的。
系統同時提供給你好幾個併發隊列。它們叫作 全局調度隊列(Global Dispatch Queues)
。目前的四個全局隊列有着不一樣的優先級:background
、low
、default
以及 high
。要知道,Apple 的 API 也會使用這些隊列,因此你添加的任何任務都不會是這些隊列中惟一的任務。
最後,你也能夠建立本身的串行隊列或併發隊列。這就是說,至少有_五個_隊列任你處置:主隊列、四個全局調度隊列,再加上任何你本身建立的隊列。
以上是調度隊列的大框架!
GCD 的「藝術」歸結爲選擇合適的隊列來調度函數以提交你的工做。體驗這一點的最好方式是走一遍下邊的列子,咱們沿途會提供一些通常性的建議。
既然本教程的目標是優化且安全的使用 GCD 調用來自不一樣線程的代碼,那麼你將從一個近乎完成的叫作 GooglyPuff
的項目入手。
GooglyPuff 是一個沒有優化,線程不安全的應用,它使用 Core Image 的人臉檢測 API 來覆蓋一對曲棍球眼睛到被檢測到的人臉上。對於基本的圖像,能夠從相機膠捲選擇,或用預設好的URL從互聯網下載。
完成項目下載以後,將其解壓到某個方便的目錄,再用 Xcode 打開它並編譯運行。這個應用看起來以下圖所示:
注意當你選擇 Le Internet
選項下載圖片時,一個 UIAlertView
過早地彈出。你將在本系列教程地第二部分修復這個問題。
這個項目中有四個有趣的類:
NSURL
的實例或一個 ALAsset
的實例來實例化照片。這個類提供一個圖像、縮略圖以及從 URL 下載的狀態。Photo
的實例.回到應用並從你的相機膠捲添加一些照片或使用 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
});
});
}
複製代碼
下面來講明上面的新代碼所作的事:
dispatch_async()
,Block 會被異步地提交,意味着調用線程地執行將會繼續。這就使得 viewDidLoad
更早地在主線程完成,讓加載過程感受起來更加快速。同時,一我的臉檢測過程會啓動並將在稍後完成。UIImageView
,那麼你就添加一個新的 Block 到主線程。記住——你必須老是在主線程訪問 UIKit 的類。fadeInNewImage:
更新 UI ,它執行一個淡入過程切換到新的曲棍球眼睛圖像。編譯並運行你的應用;選擇一個圖像而後你會注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在以後就加上了。這給應用帶來了不錯的效果,和以前的顯示差異巨大。
進一步,若是你試着加載一個超大的圖像,應用不會在加載視圖控制器上「掛住」,這就使得應用具備很好伸縮性。
正如以前提到的, dispatch_async
添加一個 Block 到隊列就當即返回了。任務會在以後由 GCD 決定執行。當你須要在後臺執行一個基於網絡或 CPU 緊張的任務時就使用 dispatch_async
,這樣就不會阻塞當前線程。
下面是一個關於在 dispatch_async
上如何以及什麼時候使用不一樣的隊列類型的快速指導:
dispatch_sync
。dispatch_async
到主隊列,你能確保這個新任務將在當前方法完成後的某個時間執行。稍微考慮一下應用的 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 被從新加載的任什麼時候候。按照註釋數字順序看看:
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 的數量有時會顯示多個。線程問題極其難以調試,由於它們每每難以重現。
要糾正這個情況,實例化代碼應該只執行一次,並阻塞其它實例在 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
的代碼)的不一樣的線程會在臨界區已有一個線程的狀況下被阻塞,直到臨界區完成爲止。
須要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類自己線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的狀況。這些須要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。
線程安全實例不是處理單例時的惟一問題。若是單例屬性表示一個可變對象,那麼你就須要考慮是否那個對象自身線程安全。
若是問題中的這個對象是一個 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 提供了同步和異步兩種障礙函數。
下圖顯示了障礙函數對多個異步隊列的影響:
注意到正常部分的操做就如同一個正常的併發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是惟一在執行的事物。在障礙完成後,隊列回到一個正常併發隊列的樣子。
下面是你什麼時候會——和不會——使用障礙函數的狀況:
因爲上面惟一像樣的選擇是自定義併發隊列,你將建立一個你本身的隊列去處理你的障礙函數並分開讀和寫函數。且這個併發隊列將容許多個多操做同時進行。
打開 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];
});
});
}
}
複製代碼
你新寫的函數是這樣工做的:
這就處理了寫操做,但你還須要實現 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;
}
複製代碼
這就是你的讀函數。按順序看看編過號的註釋,有這些:
__block
關鍵字容許對象在 Block 內可變。沒有它,array
在 Block 內部就只是只讀的,你的代碼甚至不能經過編譯。concurrentPhotoQueue
上同步調度來執行讀操做。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
單例如今是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。
依然沒有 100% 地掌握 GCD 的要領?確保你可使用 GCD 函數輕鬆地建立簡單的例子,使用斷點和 NSLog
語句保證本身明白當下發生的狀況。
我在下面提供了兩個 GIF動畫來幫助你鞏固對 dispatch_async
和 dispatch_sync
的理解。包含在每一個 GIF 中的代碼能夠提供視覺輔助;仔細注意 GIF 左邊顯示代碼斷點的每一步,以及右邊相關隊列的狀態。
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
複製代碼
下面是圖中幾個步驟的說明:
UIViewController
的任務,其中包含了 viewDidLoad
。viewDidLoad
在主線程執行。viewDidLoad
內,正要到達 dispatch_sync
。dispatch_sync
Block 被添加到一個全局隊列中,將在稍後執行。進程將在主線程掛起直到該 Block 完成。同時,全局隊列併發處理任務;要記得 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。dispatch_sync
Block 加入以前已經出如今隊列中的任務。dispatch_sync
Block 。viewDidLoad
方法完成,主隊列繼續處理其餘任務。dispatch_sync
添加任務到一個隊列並等待直到任務完成。dispatch_async
作相似的事情,但不一樣之處是它不會等待任務的完成,而是當即繼續「調用線程」的其它任務。
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"First Log");
});
NSLog(@"Second Log");
}
複製代碼
UIViewController
的任務,其中包含了 viewDidLoad
。viewDidLoad
在主線程執行。viewDidLoad
內,正要到達 dispatch_async
。dispatch_async
Block 被添加到一個全局隊列中,將在稍後執行。viewDidLoad
在添加 dispatch_async
到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列併發地處理它未完成地任務。記住 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。dispatch_async
的代碼塊開始執行。dispatch_async
Block 完成,兩個 NSLog
語句將它們的輸出放在控制檯上。在這個特定的實例中,第二個 NSLog
語句執行,跟着是第一個 NSLog
語句。並不老是這樣——着取決於給定時刻硬件正在作的事情,並且你沒法控制或知曉哪一個語句會先執行。「第一個」 NSLog
在某些調用狀況下會第一個執行。
在本教程中,你學習瞭如何讓你的代碼線程安全,以及在執行 CPU 密集型任務時如何保持主線程的響應性。
你能夠下載 GooglyPuff 項目,它包含了目前全部本教程中編寫的實現。在本教程的第二部分,你將繼續改進這個項目。
若是你計劃優化你本身的應用,那你應該用 Instruments
中的 Time Profile
模版分析你的工做。對這個工具的使用超出了本教程的範圍,你能夠看看 如何使用Instruments 來獲得一個很好的概述。
同時請確保在真實設備上分析,而在模擬器上測試會對程序速度產生很是不許確的印象。
在教程的下一部分,你將更加深刻到 GCD 的 API 中,作一些更 Cool 的東西。
若是你有任何問題或評論,可自由地加入下方的討論!