本文摘錄自《Objective-C高級(jí)編程》一書,附加一些自己的理解,作為對(duì)GCD的總結(jié)。
此篇主要包含以下幾個(gè)方面:
dispatch_suspend / dispatch_resume
dispatch_once
-
Dispatch Semaphore
- dispatch_semaphore_t
- dispatch_semaphore_create()
- dispatch_semaphore_wait()
- dispatch_semaphore_signal()
dispatch_suspend / dispatch_resume
當(dāng)追加大量處理到Dispatch Queue時(shí),在追加處理的過程中,有時(shí)希望不執(zhí)行已追加的處理。例如演算結(jié)果被Block截獲時(shí),一些處理會(huì)對(duì)這個(gè)演算結(jié)果造成影響。
在這種情況下,只要掛起Dispatch Queue即可。當(dāng)可以執(zhí)行時(shí)再恢復(fù)。
dispatch_suspend 函數(shù)掛起指定的Dispatch Queue。
dispatch_suspend(queue);
dispatch_resume 函數(shù)恢復(fù)指定的Dispatch Queue。
dispatch_resume(queue);
這些函數(shù)對(duì)已經(jīng)執(zhí)行的處理沒有影響。掛起后,追加到Dispatch Queue中但尚未執(zhí)行的處理在此之后停止執(zhí)行。而恢復(fù)則使得這些處理能夠繼續(xù)執(zhí)行。
注:
dispatch_suspend函數(shù)和dispatch_resume函數(shù)都可以用在Dispatch Source,而掛起和恢復(fù)的就是dispatch_source_set_event_handler函數(shù)的回調(diào)。
dispatch_once
dispatch_once函數(shù)是保證在應(yīng)用程序執(zhí)行中只執(zhí)行一次指定處理的API。下面這種經(jīng)常出現(xiàn)的用來(lái)進(jìn)行初始化的源代碼可通過dispatch_once函數(shù)簡(jiǎn)化。
static int initialized = NO;
if (initialized == NO) {
/*
* 初始化
*/
initialized = YES;
}
如果使用dispatch_once函數(shù),則源代碼寫為:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
* 初始化
*/
});
源代碼看起來(lái)沒有太大的變化。但是通過dispatch_once函數(shù),該源代碼即使在多線程環(huán)境下執(zhí)行,也可保證百分之百安全。
之前的源代碼在大多數(shù)情況下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的標(biāo)志變量時(shí)讀取,就有可能多次執(zhí)行初始化處理。而用dispatch_once函數(shù)初始化就不必?fù)?dān)心這樣的問題。這就是所說(shuō)的單例模式,在生成單例對(duì)象時(shí)使用。
Dispatch Semaphore
當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時(shí)即多個(gè)線程同時(shí)訪問同一數(shù)據(jù),會(huì)產(chǎn)生數(shù)據(jù)不一致的情況,有時(shí)應(yīng)用程序還會(huì)異常結(jié)束。雖然使用Serial Dispatch Queue 和dispatch_barier_async函數(shù)可避免這類問題,但有必要進(jìn)行更細(xì)粒度的排他控制。
我們來(lái)思考一下這種情況:使用兩個(gè)線程去訪問同一個(gè)數(shù)據(jù),以下代碼countNumber最終結(jié)果是多少。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 10000; i++) {
self.countNumber = self.countNumber + 1;
NSLog(@"%d",self.countNumber);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 10000; i++) {
self.countNumber = self.countNumber + 1;
NSLog(@"%d",self.countNumber);
}
});

最終結(jié)果并不是20000,這就是典型的線程安全問題。
因?yàn)樵撛创a使用Global Dispatch Queue 更新countNumber屬性,所以執(zhí)行后數(shù)據(jù)有很高概率并不是實(shí)時(shí)有效的,程序很可能異常結(jié)束。此時(shí)應(yīng)使用Dispatch Semaphore。
Dispatch Semaphore本來(lái)使用的是更細(xì)粒度的對(duì)象,不過本書還是使用該源代碼對(duì)Dispatch Semaphore進(jìn)行說(shuō)明。
Dispatch Semaphore是持有計(jì)數(shù)的信號(hào),該計(jì)數(shù)是多線程編程中的計(jì)數(shù)類型信號(hào)。所謂信號(hào),類似于過馬路時(shí)常用的手旗。可以通過時(shí)舉起手旗,不可通過時(shí)放下手旗。而在Dispatch Semaphore中,使用計(jì)數(shù)來(lái)實(shí)現(xiàn)該功能。計(jì)數(shù)為0時(shí)等待,計(jì)數(shù)為1或大于1時(shí)代碼可通過。
下面介紹一下使用方法。通過dispatch_semaphore_create函數(shù)生成Dispatch Semaphore。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
參數(shù)表示計(jì)數(shù)的初始值。本例將計(jì)數(shù)值初始化為“1”。
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait函數(shù)等待Dispatch Semaphore的計(jì)數(shù)值達(dá)到大于或等于1。當(dāng)計(jì)數(shù)值大于等于1,或者在待機(jī)中計(jì)數(shù)值大于等于1時(shí),對(duì)該計(jì)數(shù)進(jìn)行減法并從dispatch_semaphore_wait函數(shù)返回。第二個(gè)參數(shù)與dispatch_group_wait函數(shù)等相同,由dispatch_time_t類型值指定等待時(shí)間。該例的參數(shù)意味著永久等待。另外,dispatch_ semaphore_wait函數(shù)的返回值也與dispatch_group_wait函數(shù)相同??上褚韵略创a這樣,通過返回值進(jìn)行分支處理。
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
long result = dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
if (result == 0) {
/*
* 由于 Dispatch Semaphore 的計(jì)數(shù)值達(dá)到大于等于1
* 或者在待機(jī)中的指定時(shí)間內(nèi) Dispatch Semaphore 的計(jì)數(shù)值達(dá)到大于等于1
* 所以 Dispatch Semaphore 的計(jì)數(shù)值減去1。
*
* 可執(zhí)行需要進(jìn)行排他控制的處理
*/
}
else {
/*
* 由于 Dispatch Semaphore 的計(jì)數(shù)值為0
* 因此在達(dá)到指定時(shí)間為止待機(jī)
*/
}
dispatch_semaphore_wait函數(shù)返回0時(shí),可安全地執(zhí)行需要進(jìn)行排他控制的處理。該處理結(jié)束時(shí)通過dispatch _semaphore_signal函數(shù)將Dispatch Semaphore的計(jì)數(shù)值加1。
我們?cè)谇懊娴脑创a中實(shí)際使用Dispatch Semaphore看看。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成 Dispatch Semaphore。
*
* Dispatch Semaphore 的計(jì)數(shù)初始值設(shè)定為“1”。
*
* 保證可訪問 countNumber 屬性的線程
* 同時(shí)只能有一個(gè)
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
* 執(zhí)行過 dispatch_semaphore_wait 后計(jì)數(shù)為 0 ,其它線程需要等待 Dispatch Semaphore ,
*
* 一直等待,直到 Dispatch Semaphore 的計(jì)數(shù)值達(dá)到大于等于 1 。
*
* 由于可訪問 countNumber 屬性的線程只有一個(gè)
* 因此可以安全的進(jìn)行更新
*/
self.countNumber += 1;
dispatch_semaphore_signal(semaphore);
/*
* 排他控制處理結(jié)束,
* 所以通過 dispatch_semaphore_signal 函數(shù)
* 將 Dispatch Semaphore 的計(jì)數(shù)值加 1。
* 如果有通過 dispatch_semaphore_wait 函數(shù)
* 等待 Dispatch Semaphore 的計(jì)數(shù)值增加的線程,
* 就由最先等待的線程執(zhí)行。
*/
NSLog(@"%d",self.countNumber);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
self.countNumber = self.countNumber + 1;
dispatch_semaphore_signal(semaphore);
NSLog(@"%d",self.countNumber);
}
});

這樣就保證了線程安全,最后的結(jié)果是20000。
在沒有Serial Dispatch Queue和 dispatch_barrier_async 函數(shù)那么大粒度且一部分處理需要進(jìn)行排他控制的情況下,Dispatch Semaphore 便可發(fā)揮威力。
《關(guān)于dispatch_semaphore的使用》中有這樣的描述:
??停車場(chǎng)剩余4個(gè)車位,那么即使同時(shí)來(lái)了四輛車也能停的下。如果此時(shí)來(lái)了五輛車,那么就有一輛需要等待。
?
??信號(hào)量的值就相當(dāng)于剩余車位的數(shù)目,dispatch_semaphore_wait函數(shù)就相當(dāng)于來(lái)了一輛車,dispatch_semaphore_signal就相當(dāng)于走了一輛車。停車位的剩余數(shù)目在初始化的時(shí)候就已經(jīng)指明了dispatch_semaphore_create(long value),調(diào)用一次dispatch_semaphore_signal,剩余的車位就增加一個(gè);調(diào)用一次dispatch_semaphore_wait剩余車位就減少一個(gè);
?
??當(dāng)剩余車位為0時(shí),再來(lái)車(即調(diào)用dispatch_semaphore_wait)就只能等待。有可能同時(shí)有幾輛車等待一個(gè)停車位。有些車主沒有耐心,給自己設(shè)定了一段等待時(shí)間,這段時(shí)間內(nèi)等不到停車位就走了,如果等到了就開進(jìn)去停車。而有些車主就像把車停在這,所以就一直等下去。
Parse源碼淺析系列(一)---Parse的底層多線程處理思路:GCD高級(jí)用法中這樣描述:
??在這個(gè)停車場(chǎng)系統(tǒng)中,車位是公共資源,每輛車好比一個(gè)線程,看門人起的就是信號(hào)量的作用。 更進(jìn)一步,信號(hào)量的特性如下:信號(hào)量是一個(gè)非負(fù)整數(shù)(車位數(shù)),所有通過它的線程(車輛)都會(huì)將該整數(shù)減一(通過它當(dāng)然是為了使用資源),當(dāng)該整數(shù)值為零時(shí),所有試圖通過它的線程都將處于等待狀態(tài)。在信號(hào)量上我們定義兩種操作: Wait(等待) 和 Release(釋放)。 當(dāng)一個(gè)線程調(diào)用Wait(等待)操作時(shí),它要么通過然后將信號(hào)量減一,要么一直等下去,直到信號(hào)量大于一或超時(shí)。Release(釋放)實(shí)際上是在信號(hào)量上執(zhí)行加操作,對(duì)應(yīng)于車輛離開停車場(chǎng),該操作之所以叫做“釋放”是因?yàn)榧硬僮鲗?shí)際上是釋放了由信號(hào)量守護(hù)的資源。
?
??從 iOS7 升到 iOS8 后,GCD 出現(xiàn)了一個(gè)重大的變化:在 iOS7 時(shí),使用 GCD 的并行隊(duì)列,dispatch_async最大開啟的線程一直能控制在6、7條,線程數(shù)都是個(gè)位數(shù),然而 iOS8后,最大線程數(shù)一度可以達(dá)到40條、50條。然而在文檔上并沒有對(duì)這一做法的目的進(jìn)行介紹。
?
??筆者推測(cè) Apple 的目的是想借此讓開發(fā)者使用NSOperationQueue:GCD 中 Apple 并沒有提供控制并發(fā)數(shù)量的接口,而NSOperationQueue有,如果需要使用 GCD 實(shí)現(xiàn),需要使用 GCD 的一項(xiàng)高級(jí)功能:Dispatch Semaphore信號(hào)量。
接下來(lái)我們使用Dispatch Semaphore來(lái)控制GCD的并發(fā)數(shù)
設(shè)置并發(fā)數(shù)為 3,即最多有三條線程同時(shí)執(zhí)行
- (void)viewDidLoad {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
unsigned int sleepTime = 2;
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
}
我們?cè)诖a中讓每條線程每?jī)擅雸?zhí)行一次,放慢速度后就容易看出同時(shí)有幾條線程在執(zhí)行

我們把
dispatch_semaphore_create()參數(shù)換成 2,即dispatch_semaphore_create(2),其它代碼原封不動(dòng),下面是運(yùn)行結(jié)果
由上述結(jié)果可以看出,我們給并發(fā)隊(duì)列異步添加了3個(gè)任務(wù),如果沒有限制的情況下會(huì)創(chuàng)建3條子線程同時(shí)執(zhí)行。當(dāng)我們把
dispatch_semaphore_create()參數(shù)設(shè)為 3 的時(shí)候,3 條線程的確同時(shí)執(zhí)行,當(dāng)我們換成 2 的時(shí)候,就只剩下兩條線程在同時(shí)執(zhí)行。這就驗(yàn)證了dispatch_semaphore_create()的參數(shù)是可以控制并發(fā)數(shù)的說(shuō)法。
所以,我們很多時(shí)候看到在網(wǎng)上各類博文中出現(xiàn)的dispatch_semaphore_create(1),這種情況大多被當(dāng)做線程鎖來(lái)使用是沒有問題的。因?yàn)閰?shù)為1,所以同時(shí)執(zhí)行的線程只能有1個(gè),達(dá)到了線程鎖要求的效果。按照dispatch_semaphore_create()的原理,與自旋鎖不同,卻類似于互斥鎖,具有線程的排他性。