Grand Central Dispatch (GCD)
目錄
- 什麼是GCD
- 什麼是多執行緒, 併發
- GCD的優勢
- GCD的API介紹
- GCD的注意點
- GCD的使用場景
- Dispatch Source
- 總結
1. 什麼是GCD
GCD, Grand Central Dispatch, 可譯為”強大的中樞排程器”, 基於libdispatch, 純C語言, 裡面包含了許多多執行緒相關非常強大的函式. 程式設計師可以既不寫一句執行緒管理的程式碼又能很好地使用多執行緒執行任務.
GCD中有Dispatch Queue和Dispatch Source. Dispatch Queue是主要的, 而Dispatch Source比較次要. 所以這裡主要介紹Dispatch Queue, 而Dispatch Source在下面會簡單介紹.
Dispatch Queue
蘋果官方對GCD的說明如下 :
開發者要做的只是定義想執行的任務並追加到適當的Dispatch Queue中.
這句話用原始碼表示如下
1 2 3 4 5 |
dispatch_async(queue, ^{ /* * 想執行的任務 */ }); |
該原始碼用block的語法定義想執行的任務然後通過dispatch_async函式講任務追加到賦值在變數queue的”Dispatch Queue”中.
Dispatch Queue究竟是什麼???
Dispatch Queue是執行處理的等待佇列, 按照先進先出(FIFO, First-In-First-Out)的順序進行任務處理.
另外, 佇列分兩種, 一種是序列佇列(Serial Dispatch Queue), 一種是並行佇列(Concurrent Dispatch Queue).
Dispatch Queue的種類 | 說明 |
---|---|
Serial Dispatch Queue | 等待現在執行中處理結束 |
Concurrent Dispatch Queue | 不等待現在執行中處理結束 |
序列佇列 : 讓任務一個接一個執行
併發佇列 : 讓多個任務同時執行(自動開啟多個執行緒執行任務)
併發功能只有在非同步函式(dispatch_async)下才有效(想想看為什麼?)
GCD的API會在下面詳細說明~
2. 什麼是多執行緒, 併發
我們知道, 一個應用就相當於一個程式, 而一個程式可以同時分發幾個執行緒同時處理任務.而併發正是一個程式開啟多個執行緒同時執行任務的意思, 主執行緒專門用來重新整理UI,處理觸控事件等 而子執行緒呢, 則用來執行耗時的操作, 例如訪問資料庫, 下載資料等..
以前我們CPU還是單核的時候, 並不存在真正的執行緒並行, 因為我們只有一個核, 一次只能處理一個任務. 所以當時我們計算機是通過分時也就是CPU地在各個程式之間快速切換, 給人一種能同時處理多工的錯覺
來實現的, 而現在多核CPU計算機則能真真正正貨真價實地辦到同時處理多個任務.
3. GCD的優勢
說到優勢, 當然有比較, 才能顯得出優勢所在. 事實上, iOS中我們能使用的多執行緒管理技術有
- pthread
- NSThread
- GCD
- NSOperationQueue
pthread
來自Clang, 純C語言, 需要手動建立執行緒, 銷燬執行緒, 手動進行執行緒管理. 而且程式碼極其噁心, 我保證你寫一次不想寫第二次…不好意思我先去吐會T~T
NSThread :
Foundation框架下的OC物件, 依舊需要自己進行執行緒管理,執行緒同步。 執行緒同步對資料的加鎖會有一定的開銷。
GCD :
兩個字, 牛逼, 雖然是純C語言, 但是它用難以置信的非常簡潔的方式實現了極其複雜的多執行緒程式設計, 而且還支援block內聯形式進行制定任務. 簡潔! 高效! 而且我們再也不用手動進行執行緒管理了.
NSOperationQueue :
相當於Foundation框架的GCD, 以物件導向的語法對GCD進行了封裝. 效率一樣高.
GCD優勢在哪裡?
- GCD會自動利用更多的CPU核心
- GCD會自動管理執行緒的生命週期
- 使用方法及其簡單
怎麼樣? 心動不, 迫不及待想要知道怎麼使用GCD了吧, 那我們馬上切入正題~
4. GCD的API介紹
在介紹GCD的API之前, 我們先搞清楚四個名詞: 序列, 並行, 同步, 非同步
- 序列 : 一個任務執行完, 再執行下一個任務
- 並行 : 多個任務同時執行
- 同步 : 在當前執行緒中執行任務, 不具備開啟執行緒的能力
- 非同步 : 在新的執行緒中執行任務, 具備開啟執行緒的能力
下面開始介紹GCD的API
建立佇列
1 |
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr) |
手動建立一個佇列.
- label : 佇列的識別符號, 日後可用來除錯程式
- attr : 佇列型別
DISPATCH_QUEUE_CONCURRENT : 併發佇列
DISPATCH_QUEUE_SERIAL 或 NULL : 序列佇列
需要注意的是, 通過dispatch_queue_create函式生成的queue在使用結束後需要通過dispatch_release函式來釋放.(只有在MRC下才需要釋放)
並不是什麼時候都需要手動建立佇列, 事實上系統給我們提供2個很常用的佇列.
主佇列
1 |
dispatch_get_main_queue(); |
該方法返回的是主執行緒中執行的同步佇列. 使用者介面的更新等一些必須在主執行緒中執行的操作追加到此佇列中.
全域性併發佇列
1 |
dispatch_get_global_queue(long identifier, unsigned long flags); |
該方法返回的是全域性併發佇列. 使用十分廣泛.
- identifier : 優先順序
DISPATCH_QUEUE_PRIORITY_HIGH : 高優先順序
DISPATCH_QUEUE_PRIORITY_DEFAULT : 預設優先順序
DISPATCH_QUEUE_PRIORITY_LOW : 低優先順序
DISPATCH_QUEUE_PRIORITY_BACKGROUND : 後臺優先順序 - flags : 暫時用不上, 傳 0 即可
注意 : 對Main Dispatch Queue和Global Dispatch Queue執行dispatch_release和dispatch_retain沒有任何問題. (MRC)
同步函式
1 |
dispatch_sync(dispatch_queue_t queue, ^(void)block); |
在引數queue佇列下同步執行block
非同步函式
1 |
dispatch_async(dispatch_queue_t queue, ^(void)block); |
在引數queue佇列下非同步執行block(開啟新執行緒)
時間
1 |
dispatch_time(dispatch_time_t when, int64_t delta); |
根據傳入的時間(when)和延遲(delta)計算出一個未來的時間
- when :
DISPATCH_TIME_NOW : 現在
DISPATCH_TIME_FOREVER : 永遠(別傳這個引數, 否則該時間很大) - delta : 該引數接收的是納秒, 可以用一個巨集NSEC_PER_SEC來進行轉換, 例如你要延遲3秒, 則為 3 * NSEC_PER_SEC.
延遲執行
1 |
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block); |
有了上述獲取時間的函式, 則可以直接把時間傳入, 然後定義該延遲執行的block在哪一個queue佇列中執行.
蘋果還給我們提供了一個在主佇列中延遲執行的程式碼塊, 如下
1 2 3 |
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 }); |
我們只需要傳入需要延遲的秒數(delayInSeconds)和執行的任務block就可以直接呼叫了, 方便吧~
注意 : 延遲執行不是在指定時間後執行任務處理, 而是在指定時間後將處理追加到佇列中, 這個是要分清楚的
佇列組
1 |
dispatch_group_create(); |
有時候我們想要在佇列中的多個任務都處理完畢之後做一些事情, 就能用到這個Group. 同佇列一樣, Group在使用完畢也是需要dispatch_release掉的(MRC). 上程式碼
組非同步函式
1 |
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block); |
分發Group內的併發非同步函式
組通知
1 |
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, ^(void)block) |
監聽group的任務進度, 當group內的任務全部完成, 則在queue佇列中執行block.
組等待
1 |
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout) |
- timeout : 等待的時間
DISPATCH_TIME_NOW : 現在
DISPATCH_TIME_FOREVER : 永遠
該函式會一直等待組內的非同步函式任務全部執行完畢才會返回. 所以該函式會卡住當前執行緒. 若引數timeout為DISPATCH_TIME_FOREVER, 則只要group內的任務尚未執行結束, 就會一直等待, 中途不能取消.
柵欄
1 |
dispatch_barrier_async(dispatch_queue_t queue, ^(void)block) |
在訪問資料庫或檔案時, 為了提高效率, 讀取操作放在並行佇列中執行. 但是寫入操作必須在序列佇列中執行(避免資源搶奪問題). 為了避免麻煩, 此時dispatch_barrier_async函式作用就出來了, 在這函式裡進行寫入操作, 寫入操作會等到所有讀取操作完畢後, 形成一道柵欄, 然後進行寫入操作, 寫入完畢後再把柵欄移除, 同時開放讀取操作. 如圖
快速迭代
1 2 3 |
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ // code here }); |
執行10次程式碼, index順序不確定. dispatch_apply會等待全部處理執行結束才會返回. 意味著dispatch_apply會阻塞當前執行緒. 所以dispatch_apply一般用於非同步函式的block中.
一次性程式碼
1 2 3 4 |
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只執行1次的程式碼(這裡面預設是執行緒安全的) }); |
該程式碼在整個程式的生命週期中只會執行一次.
掛起和恢復
1 |
dispatch_suspend(queue) |
掛起指定的queue佇列, 對已經執行的沒有影響, 追加到佇列中尚未執行的停止執行.
1 |
dispatch_resume(queue) |
恢復指定的queue佇列, 使尚未執行的處理繼續執行.
5. GCD的注意點
因為在ARC下, 不需要我們釋放自己建立的佇列, 所以GCD的注意點就剩下死鎖
死鎖
1 2 3 4 5 |
NSLog(@"111"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"222"); }); NSLog(@"333"); |
以上三行程式碼將輸出什麼?
111
222
333
?
還是
111
333
?
其實都不對, 輸出結果是
111
為什麼? 看下圖
毫無疑問會先輸出111, 然後在當前佇列下呼叫dispatch_sync函式, dispatch_sync函式會把block追加到當前佇列上, 然後等待block呼叫完畢該函式才會返回, 不巧的是, block在佇列的尾端, 而佇列正在執行的是dispatch_sync函式. 現在的情況是, block不執行完畢, dispatch_sync函式就不能返回, dispatch_sync不返回, 就沒機會執行block函式. 這種你等我, 我也等你的情況就是死鎖, 後果就是大家都執行不了, 當前執行緒卡死在這裡.
如何避免死鎖?
不要在當前佇列使用同步函式, 在佇列巢狀的情況下也不允許. 如下圖,
大家可以想象, 佇列1執行完NSLog後到佇列2中執行NSLog, 佇列2執行完後又跳回佇列1中執行NSLog, 由於都是同步函式, 所以最內層的NSLog(“333”); 追加到佇列1中, 實際上最外層的dispatch_sync是還沒返回的, 所以它沒有執行的機會. 也形成死鎖. 執行程式, 果不其然, 列印如下 :
111
222
6. GCD的使用場景
執行緒間的通訊
這是GCD最常用的使用場景了, 如下程式碼
1 2 3 4 5 6 |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行耗時操作 dispatch_async(dispatch_get_main_queue(), ^{ // 回到主執行緒作重新整理UI等操作 }); }); |
為了不阻塞主執行緒, 我們總是在後臺執行緒中傳送網路請求, 處理資料, 然後再回到主執行緒中重新整理UI介面
單例
單例也就是在程式的整個生命週期中, 該類有且僅有一個例項物件, 此時為了保證只有一個例項物件, 我們這裡用到了dispatch_once函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static XXTool <em>_instance; + (instancetype)allocWithZone:(struct _NSZone </em>)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; } |
因為alloc內部會呼叫allWithZone, 所以我們重寫allocWithZone方法就行了. 通過以上程式碼可以保證程式只能建立一個例項物件, 並且該例項物件永遠存在程式中.
同步佇列和鎖
我們知道, 屬性中有atomic和nonatomic屬性
- atomic : setter方法執行緒安全, 需要消耗大量的資源
- nonatomic : setter方法非執行緒安全, 適合記憶體小的移動裝置
為了實現屬性執行緒安全, 避免資源搶奪的問題, 我們也許會這樣寫
1 2 3 4 5 6 |
- (NSString *)setMyString:(NSString *)myString { @synchronized(self) { _myString = myString; } } |
這種方法沒錯是可以達到該屬性執行緒安全的需求, 但是試想一下, 如果一個物件中有許多個屬性都需要保證執行緒安全, 那麼就會在self物件上頻繁加鎖, 那麼兩個毫無關係的setter方法就有可能執行一個setter方法需要等待另一個setter方法執行完畢解鎖之後才能執行, 這樣做毫無必要. 那麼你有可能會說, 在每個方法內部建立一個鎖物件就好啦, 不過你不覺得這樣會浪費資源嗎?
那麼能不能利用佇列, 實現getter方法可以併發執行, 而setter方法序列執行並且setter和getter不能併發執行呢??? 沒錯, 我們這裡用到了dispatch_barrier_async函式.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (NSString <em>)myString { __block NSString </em>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; }); } |
這裡利用了柵欄塊必須單獨執行, 不能與其他塊並行的特性, 寫入操作就必須等當前的讀取操作都執行完畢, 然後單獨執行寫入操作, 等待寫入操作執行完畢後再繼續處理讀取.
7. Dispatch Source
它是BSD系核心慣有功能kqueue的包裝. kqueue的CPU負荷非常小, 可以說是應用程式處理XNU核心中發生的各種事件的方法中最優秀的一種.
但是由於Dispatch Source實在是太少人用了, 所以這裡不再介紹. 感興趣的朋友們可以自行Google.
8. 總結
- GCD可進行執行緒間通訊
- GCD可以辦到執行緒安全
- GCD可用於延遲執行
- GCD需要注意死鎖問題(不要在當前佇列呼叫同步函式)
想再往深瞭解併發程式設計, 可以看看這篇文章
併發程式設計 : API及挑戰
附上另外兩篇文章的連結
Objective-C高階程式設計讀書筆記之記憶體管理
Objective-C高階程式設計讀書筆記之blocks
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式