本文摘錄自《Objective-C高級編程》一書菊霜,附加一些自己的理解,作為對GCD的總結(jié)济赎。
此篇主要包含以下幾個方面:
dispatch_suspend / dispatch_resume
dispatch_once
-
Dispatch Semaphore
- dispatch_semaphore_t
- dispatch_semaphore_create()
- dispatch_semaphore_wait()
- dispatch_semaphore_signal()
dispatch_suspend / dispatch_resume
當追加大量處理到Dispatch Queue時鉴逞,在追加處理的過程中,有時希望不執(zhí)行已追加的處理联喘。例如演算結(jié)果被Block截獲時华蜒,一些處理會對這個演算結(jié)果造成影響辙纬。
在這種情況下豁遭,只要掛起Dispatch Queue即可。當可以執(zhí)行時再恢復贺拣。
dispatch_suspend
函數(shù)掛起指定的Dispatch Queue蓖谢。
dispatch_suspend(queue);
dispatch_resume
函數(shù)恢復指定的Dispatch Queue譬涡。
dispatch_resume(queue)闪幽;
這些函數(shù)對已經(jīng)執(zhí)行的處理沒有影響。掛起后涡匀,追加到Dispatch Queue中但尚未執(zhí)行的處理在此之后停止執(zhí)行盯腌。而恢復則使得這些處理能夠繼續(xù)執(zhí)行。
注:
dispatch_suspend
函數(shù)和dispatch_resume
函數(shù)都可以用在Dispatch Source陨瘩,而掛起和恢復的就是dispatch_source_set_event_handler
函數(shù)的回調(diào)腕够。
dispatch_once
dispatch_once
函數(shù)是保證在應用程序執(zhí)行中只執(zhí)行一次指定處理的API级乍。下面這種經(jīng)常出現(xiàn)的用來進行初始化的源代碼可通過dispatch_once
函數(shù)簡化。
static int initialized = NO;
if (initialized == NO) {
/*
* 初始化
*/
initialized = YES;
}
如果使用dispatch_once
函數(shù)帚湘,則源代碼寫為:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
* 初始化
*/
});
源代碼看起來沒有太大的變化玫荣。但是通過dispatch_once
函數(shù),該源代碼即使在多線程環(huán)境下執(zhí)行大诸,也可保證百分之百安全捅厂。
之前的源代碼在大多數(shù)情況下也是安全的。但是在多核CPU中资柔,在正在更新表示是否初始化的標志變量時讀取焙贷,就有可能多次執(zhí)行初始化處理。而用dispatch_once
函數(shù)初始化就不必擔心這樣的問題贿堰。這就是所說的單例模式盈厘,在生成單例對象時使用。
Dispatch Semaphore
當并行執(zhí)行的處理更新數(shù)據(jù)時即多個線程同時訪問同一數(shù)據(jù)官边,會產(chǎn)生數(shù)據(jù)不一致的情況沸手,有時應用程序還會異常結(jié)束。雖然使用Serial Dispatch Queue 和dispatch_barier_async
函數(shù)可避免這類問題注簿,但有必要進行更細粒度的排他控制契吉。
我們來思考一下這種情況:使用兩個線程去訪問同一個數(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捐晶,這就是典型的線程安全問題。
因為該源代碼使用Global Dispatch Queue 更新countNumber屬性妄辩,所以執(zhí)行后數(shù)據(jù)有很高概率并不是實時有效的惑灵,程序很可能異常結(jié)束。此時應使用Dispatch Semaphore眼耀。
Dispatch Semaphore本來使用的是更細粒度的對象英支,不過本書還是使用該源代碼對Dispatch Semaphore進行說明。
Dispatch Semaphore是持有計數(shù)的信號哮伟,該計數(shù)是多線程編程中的計數(shù)類型信號干花。所謂信號,類似于過馬路時常用的手旗楞黄〕仄啵可以通過時舉起手旗,不可通過時放下手旗鬼廓。而在Dispatch Semaphore中肿仑,使用計數(shù)來實現(xiàn)該功能。計數(shù)為0時等待,計數(shù)為1或大于1時代碼可通過尤慰。
下面介紹一下使用方法勾邦。通過dispatch_semaphore_create
函數(shù)生成Dispatch Semaphore。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
參數(shù)表示計數(shù)的初始值割择。本例將計數(shù)值初始化為“1”眷篇。
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait
函數(shù)等待Dispatch Semaphore的計數(shù)值達到大于或等于1。當計數(shù)值大于等于1荔泳,或者在待機中計數(shù)值大于等于1時蕉饼,對該計數(shù)進行減法并從dispatch_semaphore_wait
函數(shù)返回。第二個參數(shù)與dispatch_group_wait
函數(shù)等相同玛歌,由dispatch_time_t
類型值指定等待時間昧港。該例的參數(shù)意味著永久等待。另外支子,dispatch_ semaphore_wait
函數(shù)的返回值也與dispatch_group_wait
函數(shù)相同创肥。可像以下源代碼這樣值朋,通過返回值進行分支處理叹侄。
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
long result = dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
if (result == 0) {
/*
* 由于 Dispatch Semaphore 的計數(shù)值達到大于等于1
* 或者在待機中的指定時間內(nèi) Dispatch Semaphore 的計數(shù)值達到大于等于1
* 所以 Dispatch Semaphore 的計數(shù)值減去1。
*
* 可執(zhí)行需要進行排他控制的處理
*/
}
else {
/*
* 由于 Dispatch Semaphore 的計數(shù)值為0
* 因此在達到指定時間為止待機
*/
}
dispatch_semaphore_wait
函數(shù)返回0時昨登,可安全地執(zhí)行需要進行排他控制的處理趾代。該處理結(jié)束時通過dispatch _semaphore_signal
函數(shù)將Dispatch Semaphore的計數(shù)值加1。
我們在前面的源代碼中實際使用Dispatch Semaphore看看丰辣。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成 Dispatch Semaphore撒强。
*
* Dispatch Semaphore 的計數(shù)初始值設定為“1”。
*
* 保證可訪問 countNumber 屬性的線程
* 同時只能有一個
*/
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 后計數(shù)為 0 笙什,其它線程需要等待 Dispatch Semaphore 飘哨,
*
* 一直等待,直到 Dispatch Semaphore 的計數(shù)值達到大于等于 1 琐凭。
*
* 由于可訪問 countNumber 屬性的線程只有一個
* 因此可以安全的進行更新
*/
self.countNumber += 1;
dispatch_semaphore_signal(semaphore);
/*
* 排他控制處理結(jié)束芽隆,
* 所以通過 dispatch_semaphore_signal 函數(shù)
* 將 Dispatch Semaphore 的計數(shù)值加 1。
* 如果有通過 dispatch_semaphore_wait 函數(shù)
* 等待 Dispatch Semaphore 的計數(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ù)那么大粒度且一部分處理需要進行排他控制的情況下,Dispatch Semaphore 便可發(fā)揮威力述呐。
《關于dispatch_semaphore的使用》中有這樣的描述:
??停車場剩余4個車位惩淳,那么即使同時來了四輛車也能停的下。如果此時來了五輛車,那么就有一輛需要等待思犁。
?
??信號量的值就相當于剩余車位的數(shù)目代虾,dispatch_semaphore_wait
函數(shù)就相當于來了一輛車,dispatch_semaphore_signal
就相當于走了一輛車激蹲。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了dispatch_semaphore_create(long value)
棉磨,調(diào)用一次dispatch_semaphore_signal
,剩余的車位就增加一個学辱;調(diào)用一次dispatch_semaphore_wait
剩余車位就減少一個乘瓤;
?
??當剩余車位為0時,再來車(即調(diào)用dispatch_semaphore_wait
)就只能等待策泣。有可能同時有幾輛車等待一個停車位衙傀。有些車主沒有耐心,給自己設定了一段等待時間萨咕,這段時間內(nèi)等不到停車位就走了统抬,如果等到了就開進去停車。而有些車主就像把車停在這危队,所以就一直等下去聪建。
Parse源碼淺析系列(一)---Parse的底層多線程處理思路:GCD高級用法中這樣描述:
??在這個停車場系統(tǒng)中,車位是公共資源茫陆,每輛車好比一個線程妆偏,看門人起的就是信號量的作用。 更進一步盅弛,信號量的特性如下:信號量是一個非負整數(shù)(車位數(shù))钱骂,所有通過它的線程(車輛)都會將該整數(shù)減一(通過它當然是為了使用資源),當該整數(shù)值為零時挪鹏,所有試圖通過它的線程都將處于等待狀態(tài)见秽。在信號量上我們定義兩種操作: Wait(等待) 和 Release(釋放)。 當一個線程調(diào)用Wait(等待)操作時讨盒,它要么通過然后將信號量減一解取,要么一直等下去,直到信號量大于一或超時返顺。Release(釋放)實際上是在信號量上執(zhí)行加操作禀苦,對應于車輛離開停車場,該操作之所以叫做“釋放”是因為加操作實際上是釋放了由信號量守護的資源遂鹊。
?
??從 iOS7 升到 iOS8 后振乏,GCD 出現(xiàn)了一個重大的變化:在 iOS7 時,使用 GCD 的并行隊列秉扑,dispatch_async
最大開啟的線程一直能控制在6慧邮、7條,線程數(shù)都是個位數(shù),然而 iOS8后误澳,最大線程數(shù)一度可以達到40條耻矮、50條。然而在文檔上并沒有對這一做法的目的進行介紹忆谓。
?
??筆者推測 Apple 的目的是想借此讓開發(fā)者使用NSOperationQueue
:GCD 中 Apple 并沒有提供控制并發(fā)數(shù)量的接口裆装,而NSOperationQueue
有,如果需要使用 GCD 實現(xiàn)倡缠,需要使用 GCD 的一項高級功能:Dispatch Semaphore信號量哨免。
接下來我們使用Dispatch Semaphore來控制GCD的并發(fā)數(shù)
設置并發(fā)數(shù)為 3,即最多有三條線程同時執(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);
}
});
}
我們在代碼中讓每條線程每兩秒執(zhí)行一次毡琉,放慢速度后就容易看出同時有幾條線程在執(zhí)行
我們把
dispatch_semaphore_create()
參數(shù)換成 2铁瞒,即dispatch_semaphore_create(2)
,其它代碼原封不動桅滋,下面是運行結(jié)果由上述結(jié)果可以看出慧耍,我們給并發(fā)隊列異步添加了3個任務,如果沒有限制的情況下會創(chuàng)建3條子線程同時執(zhí)行丐谋。當我們把
dispatch_semaphore_create()
參數(shù)設為 3 的時候芍碧,3 條線程的確同時執(zhí)行,當我們換成 2 的時候号俐,就只剩下兩條線程在同時執(zhí)行泌豆。這就驗證了dispatch_semaphore_create()
的參數(shù)是可以控制并發(fā)數(shù)的說法。
所以吏饿,我們很多時候看到在網(wǎng)上各類博文中出現(xiàn)的dispatch_semaphore_create(1)
踪危,這種情況大多被當做線程鎖來使用是沒有問題的。因為參數(shù)為1猪落,所以同時執(zhí)行的線程只能有1個贞远,達到了線程鎖要求的效果。按照dispatch_semaphore_create()
的原理笨忌,與自旋鎖不同蓝仲,卻類似于互斥鎖,具有線程的排他性官疲。