基于自 raywenderlich.com 在2015年的兩篇文章 Grand Central Dispatch Tutorial for Swift 3: Part 1/2 和 Part 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 版本的頭文件里看對應的文檔,非常詳細。
基本概念
- Serial vs. Concurrent
這兩個詞用來描述執行多個任務時任務之間的關系:Serial,常譯作「串行」,表示這些任務同時最多只能有一個任務在執行;Concurrent,在這種語境下常譯作「并行」,表示這些任務有可能同時執行多個。 - Synchronous vs. Asynchronous
這兩個詞用來描述函數返回的時機以及函數的運作方式:Synchronous 常譯作「同步」,表示函數占用當前線程直到運行結束才返回結果;Asynchronous 常譯作「異步」,表示函數立即返回結果,而把實際的任務放在其他線程里運行。 -
Concurrency vs. Parallelism
兩者的區別在于,前者需要進行上下文切換造成同時執行兩個或多個線程的假象。Parallelism 在多核設備上才能進行,而得益于多核,Concurrency 也可以采用后者一樣的方式,這取決于系統。
Concurrency vs. Parallelism - GCD and Queue
GCD 全稱 Grand Central Dispatch, 是 libdispatch 這個庫的外部代號,它提供 dispatch queues 來執行任務;dispatch queue 都是線程安全的,并且保證加入的任務按照 FIFO 規則來運行;dispatch queue 無法保證任務的精確執行時間,需要控制執行時間就直接使用 NSThread;dispatch queue 分為 serial queue 及 concurrent queue 兩類,與第1點的概念匹配。 - Serial Queues vs. Concurrent Queues
serial queues 保證一次只執行一個任務,當前任務結束后才能執行下一個任務,但不保證兩個任務之間的間隔時間;concurrent queue 唯一能保證的是加入的任務的執行順序是按照它們加入的時間來的。
Dispatch Queue 優選
- 預定義 Queue
系統提供了5種級別的 Dispatch Queue。
其中 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。
關于 QOS 的具體解釋,可以查詢這篇文檔 Prioritize Work with Quality of Service Classes。
更新 UI 切記一定要在 main queue 里進行,不然很有可能跟你的預期不一樣。想我還是個大菜鳥的時候,就犯了這個錯誤,死活找不到原因。在 Xcode 9 里有了 Main Thread Checker 可以檢測到不在主線程更新 UI 的代碼,貌似默認是開啟的,在 Edit Scheme -> Run -> Disgnostics 里。
- 自定義 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()
,盡管這兩個是等價的。
常規使用
- 任務封裝 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
。
- 在 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 執行時是唯一執行的任務,如下圖所示。
在 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 的作用。
其它
- 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)
Dispatch Source
Dispatch Source 用來監視一些系統底層事件并自動做出反應:在 dispatch queue 中提交 Block 對事件作出處理,感覺很熟悉是吧。我還沒處理底層的經驗,文章使用的例子是利用 dispatch source 對應用恢復運行狀態做出反應,然而還是不懂這個的用處。作者表示為了在現實中能派上用場利用 dispatch source 實現了一個 stack trace tool 用于調試,然而,我看不懂,覺得總結不出個啥來。-
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