整理自raywenderlich。
1.GCD是嘛?
GCD是Grand Central Dispatch的縮寫,是蘋果對多核硬件上執(zhí)行并發(fā)代碼的一種支持。
它有以下優(yōu)點:
- GCD通過把計算密集型任務(wù)放于后臺運行,以此提高APP的響應(yīng)速度。
- GCD提供了更簡單的并發(fā)模型,它優(yōu)于線程鎖,并且?guī)椭惚苊獠l(fā)bug。
- GCD基于底層、高性能的優(yōu)化常規(guī)類型的代碼,例如單例。
2.GCD相關(guān)術(shù)語
串行和并發(fā)(Serial vs. Concurrent)
串行和并發(fā)描述了任務(wù)之間執(zhí)行的時機。任務(wù)如果是串行的,那么在同一時間只執(zhí)行一個任務(wù)。并發(fā)的多個任務(wù)被執(zhí)行的時候,可能是在同一時間。(覺得有點繞?沒關(guān)系,后面有美圖-_-`)。
同步和異步(Synchronous vs. Asynchronous)
同步和異步描述了一個函數(shù)相對于另一個函數(shù)何時執(zhí)行完畢。
同步的函數(shù)只有當它調(diào)用的任務(wù)執(zhí)行完,才會返回。
而異步函數(shù),會立即返回。雖然它也命令任務(wù)執(zhí)行完,但它并不等待任務(wù)執(zhí)行完。如此,異步函數(shù)就不會阻塞當前線程。
危險區(qū)(Critical Section)
它是指一段代碼一定不能被并發(fā)執(zhí)行,也就是說,它不能同時在兩個線程里。這是因為代碼在并發(fā)操作共享資源(例如,NSMutableArray)時,該資源可能會損壞。
競態(tài)條件(Race Condition)
它指這樣一種情形:在軟件系統(tǒng)中,指定序列的執(zhí)行行為或者事件的執(zhí)行時間,沒有被有效規(guī)范(例如,并發(fā)任務(wù)中的執(zhí)行命令)。競態(tài)條件會產(chǎn)生一些不可預(yù)測的行為,這些行為通過代碼檢查是很難發(fā)現(xiàn)的。
死鎖(Deadlock)
兩個或多個線程滿足以下情況,就被稱為死鎖:它們陷入了相互等待對方完成或執(zhí)行一個動作。第一個無法完成是因為它在等待第二個完成。而第二個無法完成是因為它在等待第一個完成。
線程安全(Thread Safe)
線程安全的代碼可以被多線程或者說并發(fā)任務(wù)安全調(diào)用,而不引起任何問題(數(shù)據(jù)損壞、崩潰等等)。線程不安全的代碼在同一時間,只能被同一環(huán)境調(diào)用。一個線程安全的例子就是NSDictionary。你可以在多線程中調(diào)用,沒有任何問題。相對來說,NSMutableDictionary就是線程不安全的,它在同一時間只能被一個線程調(diào)用。
環(huán)境切換(Context Switch)
當你在不同的線程中切換執(zhí)行的時候,對執(zhí)行狀態(tài)的儲存和恢復(fù)的處理,稱為環(huán)境切換。當你寫多任務(wù)APP時,這種處理相當普遍,但是要付出一些額外的成本。
3.并發(fā)執(zhí)行和平行執(zhí)行(Concurrency vs Parallelism)
并發(fā)執(zhí)行和平行執(zhí)行經(jīng)常相提并論,所以值得我們簡單的區(qū)分一下。
并發(fā)執(zhí)行的每一部分是被"同時執(zhí)行"的。事實上,這種"同時"取決于并發(fā)執(zhí)行時的系統(tǒng)。
多核設(shè)備執(zhí)行多線程利用的是平行執(zhí)行,但是,為了單核設(shè)備也能實現(xiàn)并發(fā),它需要執(zhí)行一會兒一個線程(A線程),然后環(huán)境切換,再去執(zhí)行另一個線程或進程(B線程),然后再回來執(zhí)行前一個線程(A線程)。它切換的很快,以至于給了你平行執(zhí)行的錯覺,就像下圖展示的:

盡管你在GCD下,寫了并發(fā)執(zhí)行的代碼,但是,是由GCD來決定平行執(zhí)行是否是必要的。平行執(zhí)行要求并發(fā)執(zhí)行,但是并發(fā)執(zhí)行并不能保證是平行執(zhí)行。
4.隊列(Queues)
GCD提供dispatch queues來操作代碼塊。這些隊列管理你提交給GCD的任務(wù),并且按照FIFO(first input first output ,先入先出)順序執(zhí)行。這保證了隊列中的第一個任務(wù)第一個被執(zhí)行,第二個任務(wù)第二個被執(zhí)行,以此類推。
所有的dispatch queues 自身都是線程安全的,所以你可以在多線程中使用它們。當你理解了dispatch queues為你提供了線程安全的代碼后,你就能理解GCD的偉大了。理解的關(guān)鍵在于你要選對dispatch queue,并且提交給queue合適的函數(shù)。
串行隊列(Serial Queues)
串行隊列同一時間只執(zhí)行一個任務(wù),只有當前一個任務(wù)執(zhí)行完,下一個任務(wù)才開始。不過,我們無法知道一個任務(wù)結(jié)束,到下一個任務(wù)開始之間所需的時間,就像下圖所示:

這些任務(wù)開始執(zhí)行的時間是由GCD控制的,你只能肯定GCD在同一時間只執(zhí)行一個任務(wù),而且執(zhí)行順序是當初它們被加到隊列中的順序。
因為在串行隊列中,不可能并發(fā)的處理兩個任務(wù),所以沒有進入到危險區(qū)(critical section)的風(fēng)險,從而保證危險區(qū)不可能進入競態(tài)條件。
并發(fā)隊列(Concurrent Queues)
并發(fā)隊列保證任務(wù)是按照加入時的順序開始的,僅此而已,你再不能從并發(fā)隊列中得到其他保證了。各個任務(wù)執(zhí)行完的順序是不可知的,下一個代碼塊何時開始執(zhí)行是不可知的,特定時間里,有多少個代碼塊在運行也是不可知的。這些還是由GCD控制的。
下圖展示了在GCD下,四個并發(fā)任務(wù)執(zhí)行的例子:

從圖中發(fā)現(xiàn),代碼塊1,2,3幾乎同時開始,但依然是一個跟著一個。代碼塊0開始了一段時間后,代碼塊1才開始。代碼塊3在代碼塊2之后開始,卻先于代碼塊2結(jié)束。
一個代碼塊何時開始執(zhí)行,完全取決于GCD。如果一個代碼塊的執(zhí)行時間與另一個重疊了,那也是由GCD來決定是否它應(yīng)該運行在另一個處理器核心,是一個核心就夠用了,還是要用環(huán)境切換來處理不同的代碼塊。
隊列類型(Queue Types)
僅僅為了讓事情更有趣(大牛!這么說話不怕閃著腰嗎?……)GCD提供了至少五中隊列類型來選擇。
首先,系統(tǒng)提供了一個特殊的串行隊列——main queue。像其他串行隊列一樣,該隊列在同一時間只能執(zhí)行一個任務(wù)。然而,它是唯一一個允許你更新UI的隊列。它就是那個你用來發(fā)送信息給UIView或者發(fā)送通知的隊列。
系統(tǒng)當然也提供了幾個并發(fā)隊列。它們被命名為Global Dispatch Queues。它們因不同的優(yōu)先級被分為四種:background,low,default,high。要知道蘋果的API也在用這些隊列,所以,這些隊列中不會僅僅有你的任務(wù)。
最終,你還可以定制自己的串行或并發(fā)隊列。這意味著你最少有五種隊列可以派遣:main queue(串行),4種global dispatch queues(并發(fā)),你定制的隊列。
dispatch_async
當你需要進行網(wǎng)絡(luò)操作或者計算密集型任務(wù)時,應(yīng)考慮用dispatch_async,使任務(wù)在后臺執(zhí)行,從而不阻塞當前線程。
在各種隊列中應(yīng)該怎么用dispatch_async,以下是一些建議:
- 定制的串行隊列(Custom Serial Queue):當你想在后臺執(zhí)行串行隊列時,
dispatch_async是個不錯的選擇。因為串行在同一時間只能執(zhí)行一個任務(wù),所以它會消除資源之間的聯(lián)系。要注意,如果你需要從方法中返回的數(shù)據(jù),你必須在串行中添加另一個代碼塊來恢復(fù)數(shù)據(jù)或者考慮用dispatch_sync。 - 主隊列(Main Queue):在并發(fā)隊列中執(zhí)行完任務(wù),我們通常會在主隊列中刷新UI。就像這樣:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
dispatch_async(dispatch_get_main_queue(), ^{
[self fadeInNewImage:overlayImage];
});
});
- 并發(fā)隊列(Concurrent Queue):在后臺執(zhí)行非UI的操作,dispatch_async是很自然的選擇。
dispatch_after
dispatch_after就像一個延遲執(zhí)行的dispatch_async。
什么樣的隊列適合使用呢?
- 定制的串行隊列(Custom Serial Queue):不建議,延遲操作沒什么意義。
- 主隊列(Main Queue):這是最好的選擇。就像這樣:
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];
}
});
- 并發(fā)隊列(Concurrent Queue):沒必要。
單例的線程安全
常見的單例實現(xiàn)如下:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
這段代碼非常簡單,我們創(chuàng)建了一個單例,并且實例化了一個私有的可變數(shù)組photosArray。
然而,if語句里的代碼并不是線程安全的。如果連續(xù)多次調(diào)用此方法,有這樣一種可能,線程A進入if語句,在單例創(chuàng)建完成前,發(fā)生環(huán)境切換( context switch),轉(zhuǎn)到線程B,線程B也會進入if語句并實例化此單例,然后系統(tǒng)再次環(huán)境切換到線程A,線程A會繼續(xù)執(zhí)行if語句后面的內(nèi)容,實例化另一個單例。這顯然就不是單例了。
if語句里的代碼就是危險區(qū)(Critical Section),當連續(xù)執(zhí)行:
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];
});
它會創(chuàng)建多個不同的“單例”,這兩個并發(fā)隊列存在競態(tài)條件(Race Condition)。
所以,dispatch_once就登場了!
dispatch_once用一種線程安全的方式,執(zhí)行且只執(zhí)行一次代碼塊。當一個線程已經(jīng)在執(zhí)行dispatch_once中的危險區(qū)(critical section),那另一個試圖進入該代碼塊的線程將被阻止,直到前一個線程執(zhí)行完畢危險區(qū)中的代碼。代碼如下:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
需要指出的是,這僅僅使進入單例變得線程安全,并沒有使這個類完全線程安全。如果單例中的屬性是一個可變對象(如NSMutableArray),那你要考慮這個對象本身是否線程安全。
如果對象是一個容器類(array,dictionary),那么很可能它是線程不安全的。蘋果提供了一個線程是否安全的清單。可以發(fā)現(xiàn),NSMutableArray,正是線程不安全的。
我們可能會這樣用_photosArray屬性:
- (void)addPhoto:(Photo *)photo
{
if (photo) {
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
}
}
以上是一個write方法,因為它改變了私有可變數(shù)組對象。
- (NSArray *)photos
{
return [NSArray arrayWithArray:_photosArray];
}
以上是一個read方法,因為它讀取了這個可變數(shù)組。它做了一份不可變的拷貝(返回了一個包含同樣對象的不可變數(shù)組),以防在調(diào)用時,不適當?shù)牟僮鞲淖兞诉@個可變數(shù)組。然而當一個線程調(diào)用write方法,另一個線程調(diào)用read方法時,這里并沒有任何的保護。
盡管很多線程同時讀取NSMutableArray的實例不會有問題,但是,當一個
線程在改變這個數(shù)組,而另外一個讀取的時候,就不盡然了。上面的方法并沒有防止這種可能。
這是軟件開發(fā)中典型的讀寫難題(Readers-Writers Problem)。GCD提供了一種優(yōu)雅的解決方案——通過dispatch barriers創(chuàng)建讀寫鎖(Readers-writer lock)。
dispatch barriers是一組函數(shù),當它與并發(fā)隊列一起用時,它就像一個串行瓶頸。用barriers的API能確保提交的代碼塊是在特定時間指定隊列中唯一被執(zhí)行的。這意味著先前提交到此隊列中的代碼塊,要在dispatch barrier執(zhí)行前,執(zhí)行完畢。如下圖所示:

注意上圖,在barrier執(zhí)行前,這個并發(fā)隊列就像一般的并發(fā)隊列那樣執(zhí)行。但是,在barrier執(zhí)行后,這個并發(fā)隊列看起來像個串行隊列了(所謂的串行瓶頸)。即,barrier是唯一一個被執(zhí)行的代碼塊。在barrier執(zhí)行完后,這個隊列又開始像一般的并發(fā)隊列一樣。
GCD提供了既有同步,也有異步的barrier函數(shù)。
在用barrier函數(shù)是應(yīng)注意什么時候能用,而什么時候不能:
- 定制的串行隊列(Custom Serial Queue):相當不建議。本身已經(jīng)串行了,還用barrier干啥?
- 系統(tǒng)提供的并發(fā)隊列(Global Concurrent Queue):要很謹慎的用。因為系統(tǒng)的并發(fā)隊列中不僅僅運行你指定的代碼塊,當你用barrier后,你會在barrier執(zhí)行時,壟斷這個線程,這可能會帶來不必要的麻煩。
- 定制的并發(fā)隊列(Custom Concurrent Queue):這是最好的選擇。
因為定制的并發(fā)隊列是我們最佳的選擇,所以,我們最好自己創(chuàng)建一個并發(fā)隊列,來操作barrier函數(shù),實現(xiàn)分離讀寫操作。
@interface PhotoManager ()
@property (nonatomic,strong,readonly) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< 一個并發(fā)隊列屬性
@end
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
// 初始化并發(fā)隊列:其中我們習(xí)慣性的用倒轉(zhuǎn)的DNS命名作為第一個參數(shù),在我們調(diào)試的時候這會很有幫助;第二個參數(shù)來指定我們創(chuàng)建串行還是并發(fā)隊列
sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
DISPATCH_QUEUE_CONCURRENT);
});
return sharedPhotoManager;
}
在write中加入barrier:
- (void)addPhoto:(Photo *)photo
{
if (photo) {
dispatch_barrier_async(self.concurrentPhotoQueue, ^{
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
});
}
}
為了write的線程安全,我們需要將read也寫在剛才那個并發(fā)隊列中(真心不知道為什么read方法也要寫在這個線程……)。我們需要從函數(shù)中得到返回值,所以我們不能用異步。
這一次,我們可以用dispatch_sync。
用dispatch_sync一定要小心,否則很有可能造成死鎖(deadlock)。如果你把現(xiàn)在正在運行的隊列(串行)作為dispatch_sync的目標隊列,那就會死鎖。dispatch_sync在等待block語句執(zhí)行完,但是block不可能執(zhí)行完(它根本就還沒開始呢),除非dispatch_sync這條調(diào)用的語句執(zhí)行完,而這是不可能的。
所以,你應(yīng)該了解什么時候、在哪里,用dispatch_sync:
- 定制的串行隊列(Custom Serial Queue)在這個隊列中,你得非常小心。如果你在這個隊列中,并且調(diào)用dispatch_sync,目標是這個隊列,那么你就造成了一個死鎖(deadlock)。
- 主隊列(Main Queue):就像在定制的串行隊列中所說的那樣,也會造成死鎖。
- 并發(fā)隊列(Concurrent Queue):無論是用dispatch barriers或者dispatch——sync,這是一個好選擇。
代碼如下:
- (NSArray *)photos
{
__block NSArray *array;
dispatch_sync(self.concurrentPhotoQueue, ^{
array = [NSArray arrayWithArray:_photosArray];
});
return array;
}
Dispatch Groups
如果你想確保一組任務(wù)完成,再執(zhí)行某些任務(wù),那么Dispatch Groups可以派上用場。Dispatch Groups里的任務(wù)可以是異步的,也可以是同步的,并且這些無論同步還是異步的任務(wù),可以來自不同的隊列。當Dispatch Groups里的任務(wù)執(zhí)行完后,它可以以異步或者同步的方式通知你。
我們需要創(chuàng)建一個dispatch_group_t的實例來追蹤這些在不同隊列中的任務(wù)。
我們先看其同步的方式——dispatch_group_wait。dispatch_group_wait會阻塞當前的線程,直到組中每個任務(wù)都完成或者超時。
我們以下面的代碼作為例子來講解:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
//由于我們用了同步的方式dispatch_group_wait,它會阻塞當前線程,所以我們在整個方法外面套上了dispatch_async,使它在后臺執(zhí)行而不會阻塞主線程。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
__block NSError *error;
//我們創(chuàng)建了一個dispatch group,這就像一個未完成任務(wù)的計數(shù)器。
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告知group,一個任務(wù)開始了。我們需要將dispatch_group_enter與dispatch_group_leave配對,否則我們會遇到莫名其妙的崩潰。
dispatch_group_enter(downloadGroup);
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
//dispatch_group_leave告知group,此任務(wù)執(zhí)行完畢,要注意與dispatch_group_enter配對。
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
//dispatch_group_wait等待所有任務(wù)完成或者超時,在此處我們設(shè)置等待時間為永遠DISPATCH_TIME_FOREVER
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);
//當以上所有任務(wù)執(zhí)行完后,我們在主隊列調(diào)用任務(wù)完成的block。
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(error);
}
});
});
}
畢竟,用同步阻塞線程,感覺會不爽,我們換異步的方式。
dispatch_group_notify提供了異步完成的block。當group中沒有其他任務(wù)時,它會執(zhí)行block。當然,我們需要為block指定完成的隊列。在這個例子中,我們把主隊列作為參數(shù)。代碼如下:
//用dispatch_group_notify,就不需要在方法外套著dispatch_async
__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);
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup);
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
//我們異步調(diào)用了group,并且在group執(zhí)行完后,返回主隊列,執(zhí)行完成后的代碼。
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(error);
}
});
dispatch_apply
dispatch_apply很像一個for循環(huán),但是它并發(fā)的執(zhí)行每一項。不過,dispatch_apply本身像for循環(huán)一樣,是串行的。當所有工作結(jié)束,dispatch_apply返回。代碼如下:
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
//第一個參數(shù)是循環(huán)次數(shù),第二個參數(shù)也可以是串行,但是那就沒有意義了,還不如用for循環(huán),第三個參數(shù)是增量變量
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);
}
});
這樣,循環(huán)變成并發(fā)的了。由于是并發(fā)的,循環(huán)的每一項的完成時機變成不可知的(for循環(huán)的時候是依次執(zhí)行,所以是順序是可知的)。
用dispatch_apply替代for是否值得呢?
在以上代碼中,用dispatch_apply并沒有很大的優(yōu)勢,反而讓隊列中跑了
很多線程,我們應(yīng)該在處理耗時較長的循環(huán)時,使用dispatch_apply。
啊哈,已經(jīng)寫了很長了,GCD很豐富,還有很多有趣的特性,例如:semaphores、Dispatch Sources等等,這篇算個開篇介紹吧,高階一點的以后再寫,敬請期待。