Objective-C高級(jí)編程讀書(shū)筆記三部曲已經(jīng)寫(xiě)完, 另外兩篇如下 :
Objective-C高級(jí)編程讀書(shū)筆記之內(nèi)存管理
Objective-C高級(jí)編程讀書(shū)筆記之blocks
Grand Central Dispatch (GCD)
目錄
- 什么是GCD
- 什么是多線(xiàn)程, 并發(fā)
- GCD的優(yōu)勢(shì)
- GCD的API介紹
- GCD的注意點(diǎn)
- GCD的使用場(chǎng)景
- Dispatch Source
- 總結(jié)
1. 什么是GCD
GCD, Grand Central Dispatch, 可譯為"強(qiáng)大的中樞調(diào)度器", 基于libdispatch, 純C語(yǔ)言, 里面包含了許多多線(xiàn)程相關(guān)非常強(qiáng)大的函數(shù). 程序員可以既不寫(xiě)一句線(xiàn)程管理的代碼又能很好地使用多線(xiàn)程執(zhí)行任務(wù).
GCD中有Dispatch Queue和Dispatch Source. Dispatch Queue是主要的, 而Dispatch Source比較次要. 所以這里主要介紹Dispatch Queue, 而Dispatch Source在下面會(huì)簡(jiǎn)單介紹.
Dispatch Queue
蘋(píng)果官方對(duì)GCD的說(shuō)明如下 :
開(kāi)發(fā)者要做的只是定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)腄ispatch Queue中.
這句話(huà)用源代碼表示如下
dispatch_async(queue, ^{
/*
* 想執(zhí)行的任務(wù)
*/
});
該源碼用block的語(yǔ)法定義想執(zhí)行的任務(wù)然后通過(guò)dispatch_async函數(shù)講任務(wù)追加到賦值在變量queue的"Dispatch Queue"中.
Dispatch Queue究竟是什么???
Dispatch Queue是執(zhí)行處理的等待隊(duì)列, 按照先進(jìn)先出(FIFO, First-In-First-Out)的順序進(jìn)行任務(wù)處理.
另外, 隊(duì)列分兩種, 一種是串行隊(duì)列(Serial Dispatch Queue), 一種是并行隊(duì)列(Concurrent Dispatch Queue).
Dispatch Queue的種類(lèi) | 說(shuō)明 |
---|---|
Serial Dispatch Queue | 等待現(xiàn)在執(zhí)行中處理結(jié)束 |
Concurrent Dispatch Queue | 不等待現(xiàn)在執(zhí)行中處理結(jié)束 |
串行隊(duì)列 : 讓任務(wù)一個(gè)接一個(gè)執(zhí)行
并發(fā)隊(duì)列 : 讓多個(gè)任務(wù)同時(shí)執(zhí)行(自動(dòng)開(kāi)啟多個(gè)線(xiàn)程執(zhí)行任務(wù))
并發(fā)功能只有在異步函數(shù)(dispatch_async)下才有效(想想看為什么?)
GCD的API會(huì)在下面詳細(xì)說(shuō)明~
2. 什么是多線(xiàn)程, 并發(fā)
我們知道, 一個(gè)應(yīng)用就相當(dāng)于一個(gè)進(jìn)程, 而一個(gè)進(jìn)程可以同時(shí)分發(fā)幾個(gè)線(xiàn)程同時(shí)處理任務(wù).而并發(fā)正是一個(gè)進(jìn)程開(kāi)啟多個(gè)線(xiàn)程同時(shí)執(zhí)行任務(wù)的意思, 主線(xiàn)程專(zhuān)門(mén)用來(lái)刷新UI,處理觸摸事件等 而子線(xiàn)程呢, 則用來(lái)執(zhí)行耗時(shí)的操作, 例如訪(fǎng)問(wèn)數(shù)據(jù)庫(kù), 下載數(shù)據(jù)等..
以前我們CPU還是單核的時(shí)候, 并不存在真正的線(xiàn)程并行, 因?yàn)槲覀冎挥幸粋€(gè)核, 一次只能處理一個(gè)任務(wù). 所以當(dāng)時(shí)我們計(jì)算機(jī)是通過(guò)分時(shí)也就是CPU地在各個(gè)進(jìn)程之間快速切換, 給人一種能同時(shí)處理多任務(wù)的錯(cuò)覺(jué)
來(lái)實(shí)現(xiàn)的, 而現(xiàn)在多核CPU計(jì)算機(jī)則能真真正正貨真價(jià)實(shí)地辦到同時(shí)處理多個(gè)任務(wù).
3. GCD的優(yōu)勢(shì)
說(shuō)到優(yōu)勢(shì), 當(dāng)然有比較, 才能顯得出優(yōu)勢(shì)所在. 事實(shí)上, iOS中我們能使用的多線(xiàn)程管理技術(shù)有
- pthread
- NSThread
- GCD
- NSOperationQueue
pthread
來(lái)自Clang, 純C語(yǔ)言, 需要手動(dòng)創(chuàng)建線(xiàn)程, 銷(xiāo)毀線(xiàn)程, 手動(dòng)進(jìn)行線(xiàn)程管理. 而且代碼極其惡心, 我保證你寫(xiě)一次不想寫(xiě)第二次...不好意思我先去吐會(huì)T~T
NSThread :
Foundation框架下的OC對(duì)象, 依舊需要自己進(jìn)行線(xiàn)程管理,線(xiàn)程同步慧起。 線(xiàn)程同步對(duì)數(shù)據(jù)的加鎖會(huì)有一定的開(kāi)銷(xiāo)崩瓤。
GCD :
兩個(gè)字, 牛逼, 雖然是純C語(yǔ)言, 但是它用難以置信的非常簡(jiǎn)潔的方式實(shí)現(xiàn)了極其復(fù)雜的多線(xiàn)程編程, 而且還支持block內(nèi)聯(lián)形式進(jìn)行制定任務(wù). 簡(jiǎn)潔! 高效! 而且我們?cè)僖膊挥檬謩?dòng)進(jìn)行線(xiàn)程管理了.
NSOperationQueue :
相當(dāng)于Foundation框架的GCD, 以面向?qū)ο蟮恼Z(yǔ)法對(duì)GCD進(jìn)行了封裝. 效率一樣高.
GCD優(yōu)勢(shì)在哪里?
- GCD會(huì)自動(dòng)利用更多的CPU內(nèi)核
- GCD會(huì)自動(dòng)管理線(xiàn)程的生命周期
- 使用方法及其簡(jiǎn)單
怎么樣? 心動(dòng)不, 迫不及待想要知道怎么使用GCD了吧, 那我們馬上切入正題~
4. GCD的API介紹
在介紹GCD的API之前, 我們先搞清楚四個(gè)名詞: 串行, 并行, 同步, 異步
- 串行 : 一個(gè)任務(wù)執(zhí)行完, 再執(zhí)行下一個(gè)任務(wù)
- 并行 : 多個(gè)任務(wù)同時(shí)執(zhí)行
- 同步 : 在當(dāng)前線(xiàn)程中執(zhí)行任務(wù), 不具備開(kāi)啟線(xiàn)程的能力
- 異步 : 在新的線(xiàn)程中執(zhí)行任務(wù), 具備開(kāi)啟線(xiàn)程的能力
下面開(kāi)始介紹GCD的API
創(chuàng)建隊(duì)列
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
手動(dòng)創(chuàng)建一個(gè)隊(duì)列.
- label : 隊(duì)列的標(biāo)識(shí)符, 日后可用來(lái)調(diào)試程序
- attr : 隊(duì)列類(lèi)型
DISPATCH_QUEUE_CONCURRENT : 并發(fā)隊(duì)列
DISPATCH_QUEUE_SERIAL 或 NULL : 串行隊(duì)列
需要注意的是, 通過(guò)dispatch_queue_create函數(shù)生成的queue在使用結(jié)束后需要通過(guò)dispatch_release函數(shù)來(lái)釋放.(只有在MRC下才需要釋放)
并不是什么時(shí)候都需要手動(dòng)創(chuàng)建隊(duì)列, 事實(shí)上系統(tǒng)給我們提供2個(gè)很常用的隊(duì)列.
主隊(duì)列
dispatch_get_main_queue();
該方法返回的是主線(xiàn)程中執(zhí)行的同步隊(duì)列. 用戶(hù)界面的更新等一些必須在主線(xiàn)程中執(zhí)行的操作追加到此隊(duì)列中.
全局并發(fā)隊(duì)列
dispatch_get_global_queue(long identifier, unsigned long flags);
該方法返回的是全局并發(fā)隊(duì)列. 使用十分廣泛.
- identifier : 優(yōu)先級(jí)
DISPATCH_QUEUE_PRIORITY_HIGH : 高優(yōu)先級(jí)
DISPATCH_QUEUE_PRIORITY_DEFAULT : 默認(rèn)優(yōu)先級(jí)
DISPATCH_QUEUE_PRIORITY_LOW : 低優(yōu)先級(jí)
DISPATCH_QUEUE_PRIORITY_BACKGROUND : 后臺(tái)優(yōu)先級(jí) - flags : 暫時(shí)用不上, 傳 0 即可
注意 : 對(duì)Main Dispatch Queue和Global Dispatch Queue執(zhí)行dispatch_release和dispatch_retain沒(méi)有任何問(wèn)題. (MRC)
同步函數(shù)
dispatch_sync(dispatch_queue_t queue, ^(void)block);
在參數(shù)queue隊(duì)列下同步執(zhí)行block
異步函數(shù)
dispatch_async(dispatch_queue_t queue, ^(void)block);
在參數(shù)queue隊(duì)列下異步執(zhí)行block(開(kāi)啟新線(xiàn)程)
時(shí)間
dispatch_time(dispatch_time_t when, int64_t delta);
根據(jù)傳入的時(shí)間(when)和延遲(delta)計(jì)算出一個(gè)未來(lái)的時(shí)間
- when :
DISPATCH_TIME_NOW : 現(xiàn)在
DISPATCH_TIME_FOREVER : 永遠(yuǎn)(別傳這個(gè)參數(shù), 否則該時(shí)間很大) - delta : 該參數(shù)接收的是納秒, 可以用一個(gè)宏NSEC_PER_SEC來(lái)進(jìn)行轉(zhuǎn)換, 例如你要延遲3秒, 則為 3 * NSEC_PER_SEC.
延遲執(zhí)行
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);
有了上述獲取時(shí)間的函數(shù), 則可以直接把時(shí)間傳入, 然后定義該延遲執(zhí)行的block在哪一個(gè)queue隊(duì)列中執(zhí)行.
蘋(píng)果還給我們提供了一個(gè)在主隊(duì)列中延遲執(zhí)行的代碼塊, 如下
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
code to be executed after a specified delay
});
我們只需要傳入需要延遲的秒數(shù)(delayInSeconds)和執(zhí)行的任務(wù)block就可以直接調(diào)用了, 方便吧~
注意 : 延遲執(zhí)行不是在指定時(shí)間后執(zhí)行任務(wù)處理, 而是在指定時(shí)間后將處理追加到隊(duì)列中, 這個(gè)是要分清楚的
隊(duì)列組
dispatch_group_create();
有時(shí)候我們想要在隊(duì)列中的多個(gè)任務(wù)都處理完畢之后做一些事情, 就能用到這個(gè)Group. 同隊(duì)列一樣, Group在使用完畢也是需要dispatch_release掉的(MRC). 上代碼
組異步函數(shù)
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block);
分發(fā)Group內(nèi)的并發(fā)異步函數(shù)
組通知
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
監(jiān)聽(tīng)group的任務(wù)進(jìn)度, 當(dāng)group內(nèi)的任務(wù)全部完成, 則在queue隊(duì)列中執(zhí)行block.
組等待
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)
- timeout : 等待的時(shí)間
DISPATCH_TIME_NOW : 現(xiàn)在
DISPATCH_TIME_FOREVER : 永遠(yuǎn)
該函數(shù)會(huì)一直等待組內(nèi)的異步函數(shù)任務(wù)全部執(zhí)行完畢才會(huì)返回. 所以該函數(shù)會(huì)卡住當(dāng)前線(xiàn)程. 若參數(shù)timeout為DISPATCH_TIME_FOREVER, 則只要group內(nèi)的任務(wù)尚未執(zhí)行結(jié)束, 就會(huì)一直等待, 中途不能取消.
柵欄
dispatch_barrier_async(dispatch_queue_t queue, ^(void)block)
在訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)或文件時(shí), 為了提高效率, 讀取操作放在并行隊(duì)列中執(zhí)行. 但是寫(xiě)入操作必須在串行隊(duì)列中執(zhí)行(避免資源搶奪問(wèn)題). 為了避免麻煩, 此時(shí)dispatch_barrier_async函數(shù)作用就出來(lái)了, 在這函數(shù)里進(jìn)行寫(xiě)入操作, 寫(xiě)入操作會(huì)等到所有讀取操作完畢后, 形成一道柵欄, 然后進(jìn)行寫(xiě)入操作, 寫(xiě)入完畢后再把柵欄移除, 同時(shí)開(kāi)放讀取操作. 如圖
快速迭代
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){
// code here
});
執(zhí)行10次代碼, index順序不確定. dispatch_apply會(huì)等待全部處理執(zhí)行結(jié)束才會(huì)返回. 意味著dispatch_apply會(huì)阻塞當(dāng)前線(xiàn)程. 所以dispatch_apply一般用于異步函數(shù)的block中.
一次性代碼
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執(zhí)行1次的代碼(這里面默認(rèn)是線(xiàn)程安全的)
});
該代碼在整個(gè)程序的生命周期中只會(huì)執(zhí)行一次.
掛起和恢復(fù)
dispatch_suspend(queue)
掛起指定的queue隊(duì)列, 對(duì)已經(jīng)執(zhí)行的沒(méi)有影響, 追加到隊(duì)列中尚未執(zhí)行的停止執(zhí)行.
dispatch_resume(queue)
恢復(fù)指定的queue隊(duì)列, 使尚未執(zhí)行的處理繼續(xù)執(zhí)行.
5. GCD的注意點(diǎn)
因?yàn)樵贏RC下, 不需要我們釋放自己創(chuàng)建的隊(duì)列, 所以GCD的注意點(diǎn)就剩下死鎖
死鎖
NSLog(@"111");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"222");
});
NSLog(@"333");
以上三行代碼將輸出什么?
111
222
333
?
還是
111
333
?
其實(shí)都不對(duì), 輸出結(jié)果是
111
為什么? 看下圖
毫無(wú)疑問(wèn)會(huì)先輸出111, 然后在當(dāng)前隊(duì)列下調(diào)用dispatch_sync函數(shù), dispatch_sync函數(shù)會(huì)把block追加到當(dāng)前隊(duì)列上, 然后等待block調(diào)用完畢該函數(shù)才會(huì)返回, 不巧的是, block在隊(duì)列的尾端, 而隊(duì)列正在執(zhí)行的是dispatch_sync函數(shù). 現(xiàn)在的情況是, block不執(zhí)行完畢, dispatch_sync函數(shù)就不能返回, dispatch_sync不返回, 就沒(méi)機(jī)會(huì)執(zhí)行block函數(shù). 這種你等我, 我也等你的情況就是死鎖, 后果就是大家都執(zhí)行不了, 當(dāng)前線(xiàn)程卡死在這里.
如何避免死鎖?
不要在當(dāng)前隊(duì)列串行隊(duì)列中使用同步函數(shù), 在隊(duì)列嵌套的情況下也不允許. 如下圖,
大家可以想象, 隊(duì)列1執(zhí)行完NSLog后到隊(duì)列2中執(zhí)行NSLog, 隊(duì)列2執(zhí)行完后又跳回隊(duì)列1中執(zhí)行NSLog, 由于都是同步函數(shù), 所以最內(nèi)層的NSLog("333"); 追加到隊(duì)列1中, 實(shí)際上最外層的dispatch_sync是還沒(méi)返回的, 所以它沒(méi)有執(zhí)行的機(jī)會(huì). 也形成死鎖. 運(yùn)行程序, 果不其然, 打印如下 :
111
222
6. GCD的使用場(chǎng)景
線(xiàn)程間的通信
這是GCD最常用的使用場(chǎng)景了, 如下代碼
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 執(zhí)行耗時(shí)操作
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主線(xiàn)程作刷新UI等操作
});
});
為了不阻塞主線(xiàn)程, 我們總是在后臺(tái)線(xiàn)程中發(fā)送網(wǎng)絡(luò)請(qǐng)求, 處理數(shù)據(jù), 然后再回到主線(xiàn)程中刷新UI界面
單例
單例也就是在程序的整個(gè)生命周期中, 該類(lèi)有且僅有一個(gè)實(shí)例對(duì)象, 此時(shí)為了保證只有一個(gè)實(shí)例對(duì)象, 我們這里用到了dispatch_once函數(shù)
static XXTool *_instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [self allocWithZone:zone];
});
return _instance;
}
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (id)copy
{
return _instance;
}
- (id)mutableCopy
{
return _instance;
}
因?yàn)閍lloc內(nèi)部會(huì)調(diào)用allWithZone, 所以我們重寫(xiě)allocWithZone方法就行了. 通過(guò)以上代碼可以保證程序只能創(chuàng)建一個(gè)實(shí)例對(duì)象, 并且該實(shí)例對(duì)象永遠(yuǎn)存在程序中.
同步隊(duì)列和鎖
我們知道, 屬性中有atomic和nonatomic屬性
- atomic : setter方法線(xiàn)程安全, 需要消耗大量的資源
- nonatomic : setter方法非線(xiàn)程安全, 適合內(nèi)存小的移動(dòng)設(shè)備
為了實(shí)現(xiàn)屬性線(xiàn)程安全, 避免資源搶奪的問(wèn)題, 我們也許會(huì)這樣寫(xiě)
- (NSString *)setMyString:(NSString *)myString
{
@synchronized(self) {
_myString = myString;
}
}
這種方法沒(méi)錯(cuò)是可以達(dá)到該屬性線(xiàn)程安全的需求, 但是試想一下, 如果一個(gè)對(duì)象中有許多個(gè)屬性都需要保證線(xiàn)程安全, 那么就會(huì)在self對(duì)象上頻繁加鎖, 那么兩個(gè)毫無(wú)關(guān)系的setter方法就有可能執(zhí)行一個(gè)setter方法需要等待另一個(gè)setter方法執(zhí)行完畢解鎖之后才能執(zhí)行, 這樣做毫無(wú)必要. 那么你有可能會(huì)說(shuō), 在每個(gè)方法內(nèi)部創(chuàng)建一個(gè)鎖對(duì)象就好啦, 不過(guò)你不覺(jué)得這樣會(huì)浪費(fèi)資源嗎?
那么能不能利用隊(duì)列, 實(shí)現(xiàn)getter方法可以并發(fā)執(zhí)行, 而setter方法串行執(zhí)行并且setter和getter不能并發(fā)執(zhí)行呢??? 沒(méi)錯(cuò), 我們這里用到了dispatch_barrier_async函數(shù).
- (NSString *)myString
{
__block NSString *localMyString = nil;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
localMyString = self.myString;
});
return localMyString;
}
- (void)setMyString:(NSString *)myString
{
dispatch_barrier_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_myString = myString;
});
}
這里利用了柵欄塊必須單獨(dú)執(zhí)行, 不能與其他塊并行的特性, 寫(xiě)入操作就必須等當(dāng)前的讀取操作都執(zhí)行完畢, 然后單獨(dú)執(zhí)行寫(xiě)入操作, 等待寫(xiě)入操作執(zhí)行完畢后再繼續(xù)處理讀取.
7. Dispatch Source
它是BSD系內(nèi)核慣有功能kqueue的包裝. kqueue的CPU負(fù)荷非常小, 可以說(shuō)是應(yīng)用程序處理XNU內(nèi)核中發(fā)生的各種事件的方法中最優(yōu)秀的一種.
但是由于Dispatch Source實(shí)在是太少人用了, 所以這里不再介紹. 感興趣的朋友們可以自行Google.
8. 總結(jié)
- GCD可進(jìn)行線(xiàn)程間通信
- GCD可以辦到線(xiàn)程安全
- GCD可用于延遲執(zhí)行
- GCD需要注意死鎖問(wèn)題(不要在當(dāng)前隊(duì)列調(diào)用同步函數(shù))
想再往深了解并發(fā)編程, 可以看看這篇文章
并發(fā)編程 : API及挑戰(zhàn)
歡迎大家關(guān)注@Jerry4me, 關(guān)注菜鳥(niǎo)成長(zhǎng)_. 我會(huì)不定時(shí)更新一些學(xué)習(xí)心得與文章.