GCD Cheat Sheet

基于自 raywenderlich.com 在2015年的兩篇文章 Grand Central Dispatch Tutorial for Swift 3: Part 1/2Part 2/2 以及WWDC14_716_716_What's new in GCD and XPC 做的筆記。

蘋果自家出的 Concurrency Programming Guide 對 GCD 介紹得非常詳細,雖然文檔內容有點過時,但依然是最好的入門文檔,強烈推薦。

GCD 在 Swift 中的 API 隨著 Swift 的版本變化很大,從 Swift 3 開始完全對象化了,不過 API 的語法好像到 Swift 3.2 才穩定,不過還有個很頭疼的問題是新 API 的官方文檔一直缺失,都好幾年了,建議去 Objective-C 版本的頭文件里看對應的文檔,非常詳細。

基本概念

  1. Serial vs. Concurrent
    這兩個詞用來描述執行多個任務時任務之間的關系:Serial,常譯作「串行」,表示這些任務同時最多只能有一個任務在執行;Concurrent,在這種語境下常譯作「并行」,表示這些任務有可能同時執行多個。
  2. Synchronous vs. Asynchronous
    這兩個詞用來描述函數返回的時機以及函數的運作方式:Synchronous 常譯作「同步」,表示函數占用當前線程直到運行結束才返回結果;Asynchronous 常譯作「異步」,表示函數立即返回結果,而把實際的任務放在其他線程里運行。
  3. Concurrency vs. Parallelism
    兩者的區別在于,前者需要進行上下文切換造成同時執行兩個或多個線程的假象。Parallelism 在多核設備上才能進行,而得益于多核,Concurrency 也可以采用后者一樣的方式,這取決于系統。


    Concurrency vs. Parallelism
  4. GCD and Queue
    GCD 全稱 Grand Central Dispatch, 是 libdispatch 這個庫的外部代號,它提供 dispatch queues 來執行任務;dispatch queue 都是線程安全的,并且保證加入的任務按照 FIFO 規則來運行;dispatch queue 無法保證任務的精確執行時間,需要控制執行時間就直接使用 NSThread;dispatch queue 分為 serial queue 及 concurrent queue 兩類,與第1點的概念匹配。
  5. Serial Queues vs. Concurrent Queues
    serial queues 保證一次只執行一個任務,當前任務結束后才能執行下一個任務,但不保證兩個任務之間的間隔時間;concurrent queue 唯一能保證的是加入的任務的執行順序是按照它們加入的時間來的。
Serial Queue

Concurrent Queue

Dispatch Queue 優選

  1. 預定義 Queue

系統提供了5種級別的 Dispatch Queue。

GCD Queues

其中 main queue,也就是用于 UI 更新的 queue,是個 serial queue, 可以通過DispatchQueue.main獲取;剩下的是不同優先級的全局并發隊列 concurrent queue,通過DispatchQueue.global(qos: DispatchQoS.QoSClass>)獲取,DispatchQoS.QoSClass ,就是以往OC 中 Dispatch Queue Priorities 在 Swift 中的表示,該參數的默認值是.default,也就是 DISPATCH_QUEUE_PRIORITY_DEFAULT。

Swift QOS map to Objective-C DISPATCH_QUEUE_PRIORITY

關于 QOS 的具體解釋,可以查詢這篇文檔 Prioritize Work with Quality of Service Classes

更新 UI 切記一定要在 main queue 里進行,不然很有可能跟你的預期不一樣。想我還是個大菜鳥的時候,就犯了這個錯誤,死活找不到原因。在 Xcode 9 里有了 Main Thread Checker 可以檢測到不在主線程更新 UI 的代碼,貌似默認是開啟的,在 Edit Scheme -> Run -> Disgnostics 里。

  1. 自定義 Queue

系統提供的唯一的 serial queue 是 main queue,為了不阻塞 main queue,就需要自制 serial queue 了。

在 Objective-C 中通過以下函數來獲取自定義 queue:

dispatch_queue_create(label: UnsafePointer<Int8>, attr: dispatch_queue_attr_t!)

serial queue 和 concurrent queue 都支持,通過后面的參數 attr 來指定,可選參數:

DISPATCH_QUEUE_SERIAL
DISPATCH_QUEUE_CONCURRENT

在 iOS 4.3之前,還不支持自定義 concurrent queue,參數 attr 只能使用0或 NULL,在一些舊的文章中該參數經常使用0或是 NULL,在 stackoverflow 上經常看到這種寫法,文章中鄭重指出了這是一種過時的寫法,嚴重缺乏可讀性;參數 label 是個指針,關于 UnsafePointer ,可以看這篇文章 OneVcat 的 書節選:UnsafePointer (打個廣告,喵大的這本書是 iOS 中文書籍里值得購買的一本)。一般來講就是使用 DNS 風格的字符串,類似"com.seedante.serialqueue"這種。這個參數主要用處在于調試時便于鑒別,起個標簽的作用。

在 Swift 中這樣來獲取自定義 serial queue:

let serialQueue = DispatchQueue.init(label: "CustomSerialQueue")

實際上這個函數的原型相當復雜,有好幾個配置項,除了 label 參數其它的都有默認值:

init(label: String, qos: DispatchQoS, attributes: DispatchQueue.Attributes, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency, target: DispatchQueue?)

獲取一個 concurrent queue 則需要在 attributes 里明確指定:

let concurrentQueue = DispatchQueue.init(label: "CustomCQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)

qos參數:這里的DispatchQoS與上面的DispatchQoS.QoSClass差不多,只是多了一個考量因素relativePriority(Int),基本上可以把它倆對等。

attributes參數:DispatchQueue.Attributes只有.concurrent,沒有.serial,解釋在這里:Add missing attribute option to DispatchQueue,簡單來說,有了.serial后,attributes 參數為[.serial, .concurrent]的話就無法執行了,所以,想要 concurrent,就明確指定,而如果要生成 serial queue,又需要指定其它參數,這個參數給個[]就可以了。在 iOS 10 后,DispatchQueue.Attributes添加了一個類性值:initiallyInactive,用這個選項創建的隊列在提交 Block 使用前必須先激活(用activate()),這個參數的意義是在于能夠讓隊列再次選擇目標隊列,這個和最后一個參數有關。

autoreleaseFrequency這個參數比較費解,查看頭文件得知這個參數用于指定如何利用 autorelease pool 處理提交的 block 的內存,三個預定義值的解釋如下:

.inherit: 繼承目標隊列的處理策略,是手動創建的 queue 的默認處理方式。
.workItem: 每個 block 執行前自動創建,執行完畢后自動釋放。
.never: 不會為每個 block 單獨設立 autorelease pool,這是全局并發隊列的處理方式。

除了.inherit,剩下的兩個都是 iOS 10 以上才能用。

最后的參數target讓我摸不著頭腦,既然已經有了qos: DispatchQoS,這個不是多此一舉嗎?我很難理解為隊列提供目標隊列這個設計的作用,這個設計可以溯源至DispatchQueue的父類DispatchObject:

DispatchObject.setTarget(queue: DispatchQueue?)

目標隊列為 DispatchObject 執行任務代碼,這個方法的文檔里提到可以為 Dispatch sources 和 Dispatch I/O channels 提供執行任務代碼的目標隊列,這兩者自身沒有線程可用,所以需要依賴目標隊列。在 Objective-C 中,手動創建的隊列可能沒有指定 priority,設定目標隊列勉強還有那么點意義。

另外,這個方法有個 Bug: 如果你希望將目標隊列設置為.default的全局隊列,要明確指定DispatchQueue.global(qos: .default),而不能使用DispatchQueue.global(),盡管這兩個是等價的。

常規使用

  1. 任務封裝 Dispatch Block 和 DispatchWorkItem

dispatch_block_t 得到了強化,添加了多個功能:

  • 等待完成,可以指定等待時間
  • 完成通知,和上一個功能合起來看如同 DispatchGroup,連 API 都一樣
  • 執行前取消
  • Qos
    在 Objective-C 中,由于自定義的 queue 可能沒有指定 priority, target queue 也可能沒有指定,這次給 dispatch_block_t 加上了 QoS 來提供最后的默認選擇。
  • flags
    為 Block 的執行增加了一些配置項目,效果類似于convenience init,實在懶得寫了,這個的文檔沒有缺失。

這些新東西在 Swift 的對應就是DispatchWorkItem類,在 Swift 中提交到 queue 的 block 自動被封裝成了DispatchWorkItem

  1. 在 Dispatch Queue 里執行任務

有了 dispatch queue,還需要正確的執行方式,GCD 日用五大金剛:

dispatch_async
dispatch_sync: 這個方法會盡可能地在當前線程執行 Block
dispatch_after
dispatch_apply:class func concurrentPerform(iterations: Int, execute work: (Int) -> Void)
dispatch_once: 在 Swift 中已移除 

前三個方法已經轉化為DispatchQueue的實例方法,dispatch_apply則成了類方法

dispatch_once 常用于實現單例模式,單例模式有個重大缺陷:無法保證線程安全。單例模式的線程安全有兩種情況需要考慮:實例的初始化過程以及讀寫過程。得益于 swift 對于安全理念的貫徹,第一個問題得到了解決;而后者得無法保證。舉個栗子:某全局變量是個類實例,在多個線程中對其內部數據進行讀寫時無法保證數據的同步,軟件開發中經典的讀寫問題。怎么解決這個問題,GCD 提供了一個優雅的方案:dispatch barriers,相關函數:

dispatch_barrier_async(queue: dispatch_queue_t, block: dispatch_block_t)
dispatch_barrier_sync(queue: dispatch_queue_t, block: dispatch_block_t)

GCD barrier 保證提交的 block 是指定的 queue 中在該 block 執行時是唯一執行的任務,如下圖所示。


Dispatch Barrier

在 Swift 中,實現單例模式已經非常簡單,使用 let 就可以了。

dispatch_apply 就是 concurrent 版本的 for loop,因此,dispatch_apply 必須放在 concurrent queue 中執行。for loop 每次 iteration 執行一個任務,而 dispatch_apply 則是將所有 iteration 的任務并行執行,所有任務完成后才返回,因此,dispatch_apply 同時也是 synchronous 的。在 Swift 中,這個 API 是如下形式:

class func concurrentPerform(iterations: Int, execute work: (Int) -> Void)

iterations代表并發的數量,work閉包里的 Int 參數起著 Index 的作用。

其它

  1. Dispatch Group

DispatchGroup 能夠追蹤多個任務的完成,支持多個 queue。

func dispatchGroupDemo(){
    let queueGroup = DispatchGroup.init()
    let serialQueue = DispatchQueue.init(label: "CustomSerialQueue")
    let concurrentQueue = DispatchQueue.init(label: "CustomCQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    
    serialQueue.async(group: queueGroup, execute: DispatchWorkItem.init(block: {
        queueGroup.enter()//告知 block 開始執行
        NSLog("Group block 0 begin")
        sleep(arc4random_uniform(UInt32(8)))
        NSLog("Group block 0 over")
        queueGroup.leave()//告知 block 已經完成了
    }))
    concurrentQueue.async(group: queueGroup, execute: DispatchWorkItem.init(block: {
        queueGroup.enter()
        NSLog("Group block 1 begin")
        sleep(arc4random_uniform(UInt32(6)))
        NSLog("Group block 2 over")
        queueGroup.leave()
    }))
    // 等待指定的時間,如果到了指定的時間跟蹤的 block 并沒有全部完成則返回 .timeout
    // 可以使用wait()一直等待直到跟蹤的所有 block 完成
    let waitResult = queueGroup.wait(timeout: .now() + 5)
    NSLog("All tasks are completed in 5s: \(waitResult)")
}

DispatchGroup 也支持異步的等待,在跟蹤的所有 block 完成后得到通知,并在指定的隊列里執行代碼。

func notify(queue: DispatchQueue, work: DispatchWorkItem)
  1. Dispatch Source
    Dispatch Source 用來監視一些系統底層事件并自動做出反應:在 dispatch queue 中提交 Block 對事件作出處理,感覺很熟悉是吧。我還沒處理底層的經驗,文章使用的例子是利用 dispatch source 對應用恢復運行狀態做出反應,然而還是不懂這個的用處。作者表示為了在現實中能派上用場利用 dispatch source 實現了一個 stack trace tool 用于調試,然而,我看不懂,覺得總結不出個啥來。

  2. Semaphores(俗稱信號量)
    作者稱 Semaphores 是 old-school threading concept,也非常復雜。我也只在有關 Unix 的文章中看到這個詞。我第一次使用這個還是為了將 ALAssetsLibrary 的異步隊列變成同步隊列,那時只是搜索來的一個答案,完全不理解。
    Semaphores 用來管制訪問有限資源的任務數量。文章中的例子說實話示范作用不大,還是官方的這個例子好,Using Dispatch Semaphores to Regulate the Use of Finite Resources,這里的代碼還是使用 Objective-C 寫的。

    // 起初總是不懂這里的初始值怎么設定,好多例子寫0,也不懂含義。實際上,這個初始值是代表著可訪問資源的數量,意義在后面體現。這里的數量表示程序同時最多可以打開的文件數量,限制這個數量避免性能問題。
    dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);
    
    // 這行代碼寫在這里讓人疑惑,在實際中,這行代碼可能在不同的線程里運行,這樣就好理解了。wait 函數將信號量的數量減1,如果此時信號量的值小于0了,表示當前資源不足,不可訪問;這里又將超時時間設定為一直等待,那么會一直等下去,同其他等待的線程一起按照 FIFO的規則排隊;或信號量的值大于0,代表還有可用資源,可以訪問,代碼繼續往下運行,程序打開一個文件,同時函數返回0表示成功,
    dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
    fd = open("/etc/services", O_RDONLY);
    
    // 處理完畢,關閉文件。然后,dispatch_semaphore_signal()將信號量加1,表示可訪問資源加1,發出信號,此時正在等待訪問該資源的其他線程將繼續競爭訪問。
    close(fd);
    dispatch_semaphore_signal(fd_sema);
    

在實際上好像運用比較多的地方是將異步函數變成同步函數,我當初就是這么用的,比較典型的就是這種:How do I wait for an asynchronously dispatched block to finish?,原理就是將信號量設為0,然后當前線程一直等待,直到異步的函數執行完畢發出信號,當前線程才結束等待,效果等同本來會立即返回的異步函數會同步地執行直到結束。

差不多就是這些,文章里還有用 XCTest 框架來測試異步代碼的內容,看看就好。接下來,可以看看《NSOperation and NSOperationQueue Tutorial in Swift》。這里還有篇《iOS 并發編程之 Operation Queues》 值得一看。
參考鏈接:
1.Grand Central Dispatch Tutorial for Swift: Part 1/2
2.Grand Central Dispatch Tutorial for Swift: Part 2/2
3.WWDC14_716_716_What's new in GCD and XPC
4.Concurrency Programming Guide

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容