本篇內容以非常初階的方式介紹 DispatchQueue,
同時會說明 iOS 體系裡最流行的多緒處理的技術 GCD 和一些應用情境和範例,
後來也有用在公司培訓初次接觸 iOS 工程師的一段課程裡。

不過本篇的主題不是最新 Swift 5.5 的 async/await,將會著重在 GCD 的 DispatchQueue

提及 DispatchQueue 前,先需要知道多執行緒的應用程式如何運作的?

  • Program:
    • 應用程式本身
  • Process:
    • 開啟應用程式後的實體
    • 每個 Process 都是獨立的,無法自由讀取其他 Process 的資源
    • 像是裝有 Thread 的容器
  • Thread:
    • 每個 Process 會有管理多個 Thread
    • 同一個 Process 底下的 Thread 就可以存取相同資源 (例如記憶體變數等等)
    • 多執行緒處理需要非常細微的操作,容易發生互搶資源,死結 DeadLock 的問題
  • Core:
    • 就是硬體上的核心,通常是多核心處理
    • 一個 Core 通常同一時間只能處理一個 Thread (還是有些例外像是 Hyper-Threading)

多執行緒程式碼可以同時間處理多項任務,然而處理多項任務就是需要操作 Thread

iOS 的多緒開發方式

  1. 第一種:Threading Programming Guide
    自行建立 NSThread 並且需要自行管理 Autorelease Pool
    需要定義 Atomic 變數或是自行管理 NSLock 來處理同步問題

  2. 第二種:Concurrency Programming Guide
    名為 Grand Central Dispatch ( GCD )
    消除建立與管理 Thread 所需程式碼,僅需定義任務,讓系統自行管理排程
    可以高效穩定地避免同步和 DeadLock 問題

  3. 第三種:Swift Concurrency
    最新的 Swift 5.5 提供了更直觀、更安全的寫法
    包含 async/await、Actor 等等好用的語法

Grand Central Dispatch ( GCD )

目前提供了三種方法處理多執行緒

  1. 第一種:Dispatch Queues
    • 利用 DispatchQueue,可以輕鬆達到 asynchronously 異步和 concurrently 並發的方式,處理你的工作
    • 依照先出原則管理
  2. 第二種:Dispatch Sources
    • 可以處理 Dispatch 的底層系統事件的通知
  3. 第三種:Operation Queues
    • 預設並行方式,不依照先進先出原則,根據優先順序決定

DispatchQueue 的建立與發動

建立方式有兩種

  1. Serial:串行的方式執行工作,一次只能執行一項任務
  2. Concurrent:並發的方式執行工作,可以同時執行多項任務
1
2
3
// 指定名稱可用於取得相同 Queue 或是避免衝突
let serialQueue = DispatchQueue(label: "serialQueue")
let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)

也可以執行呼叫出系統預設的 DispatchQueue

  1. DispatchQueue.main
    • 屬於 Serial 方式
    • 全域可以操作的主要 Queue
    • UI相關的操作必須要在這裡操作
  2. DispatchQueue.global
    • 屬於 Concurrent 方式
    • 適合在背景處理大量計算
1
2
let mainQueue = DispatchQueue.main //<——- 為 serial
let globalQueue = DispatchQueue.global() //<——- 為 concurrent

發動方式有兩種

  1. Synchronously 同步發動:發動工作後,必須執行完成工作,才能往下執行
    • queue.sync { 定義你要做的工作 }
  2. Asynchronously 異步發動:發動工作後,直接執行後續動作
    • queue.async { 定義你要做的工作 }

DispatchQueue 組合運用範例

關鍵判斷方式:
- 決定 Closure 裡面可不可以執行看 Serial vs Concurrent
- 決定後續是否直接執行看 Sync vs Async

1
2
3
4
5
6
7
8
9
let serialQueue = DispatchQueue(label: "serialQueue")
print("start")
serialQueue.sync {
(1 ... 5).forEach{ print("i: " + "\($0)") }
}
serialQueue.sync {
(1 ... 5).forEach{ print("j: " + "\($0)") }
}
print("end")

由於執行 sync 所以 Closure1 內要先執行完成,才可以往下走
start > Closure1 > Closure2 > end

1
2
3
4
5
6
7
8
9
10
11
12
start
i: 1
i: 2
i: 3
i: 4
i: 5
j: 1
j: 2
j: 3
j: 4
j: 5
end
1
2
3
4
5
6
7
8
9
let serialQueue = DispatchQueue(label: "serialQueue")
print("start")
serialQueue.async {
(1 ... 5).forEach{ print("i: " + "\($0)") }
}
serialQueue.async {
(1 ... 5).forEach{ print("j: " + "\($0)") }
}
print("end")

由於執行 async 所以可以連續執行到下一個 async 和最後一個 print end,
但因為是 serialQueue,所以導致先執行完第一個 Closure1 才能執行 Closure2

1
2
3
4
5
6
7
8
9
10
11
12
start
end
i: 1
i: 2
i: 3
i: 4
i: 5
j: 1
j: 2
j: 3
j: 4
j: 5
1
2
3
4
5
6
7
8
9
let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
print("start")
concurrentQueue.sync {
(1 ... 5).forEach{ print("i: " + "\($0)") }
}
concurrentQueue.sync {
(1 ... 5).forEach{ print("j: " + "\($0)") }
}
print("end")

由於執行 sync 所以 Closure1 內要先執行完成,才可以往下走
所以就算是 concurrentQueue 也一樣

1
2
3
4
5
6
7
8
9
10
11
12
start
i: 1
i: 2
i: 3
i: 4
i: 5
j: 1
j: 2
j: 3
j: 4
j: 5
end
1
2
3
4
5
6
7
8
9
let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
print("start")
concurrentQueue.async {
(1 ... 5).forEach{ print("i: " + "\($0)") }
}
concurrentQueue.async {
(1 ... 5).forEach{ print("j: " + "\($0)") }
}
print("end")

由於執行 async 所以可以連續執行到下一個 async 和最後一個 print end,
並且因為是 concurrentQueue 所以 Closure1 和 Closure2 是同時進行的。

1
2
3
4
5
6
7
8
9
10
11
12
start
end
j: 1
i: 1
j: 2
i: 2
j: 3
i: 3
j: 4
i: 4
j: 5
i: 5

為什麼 async 的發動方式可以連續執行後續的程式碼呢?

原因是因為 async 會特別建立新的 thread 來處理 async 裡的工作,
因此當前的執行緒可以繼續執行後續的程式碼。


知道上面那些組合,那他的應用情境到底是哪裡?


DispatchQueue 應用情境

情境一:處理大量資料

通常不希望處理大量資料時會卡住使用者畫面,所以需要在背景執行工作。
可以使用 DispatchQueue.global().async
但記得要回 main thread 才能更新畫面。

1
2
3
4
5
6
7
8
9
10
11
12
func handleBigData(){
DispatchQueue.global().async {
var total = 0
for i in 0...1000000{
total += i
}
print("Done BigData \(total)")
DispatchQueue.main.async {
// TODO: Update UI on main thread.
}
}
}

情境二:呼叫API

最常使用的 API 三方 Alamofire 幫我們處理好了,
呼叫 API 前三方內部會用 async 讓畫面不會卡,
同時預設回應是回 main queue (也可以自行設定 queue)。

1
2
3
4
5
6
7
func handleCallApi(){
guard let url = URL(string: "https://httpbin.org/get") else { return }
AF.request(url).response{
response in
print("Done CallApi url \(response)")
}
}

情境三:多道API並行呼叫,全部完成時要做統一處理

多道API呼叫後的結果,可能需要最後做一個統整的處理,最後才能更新畫面。

可以利用 group(enter+leave+notify)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func handleMergeApi(){
guard let url1 = URL(string: "https://httpbin.org/get?data=1") else { return }
guard let url2 = URL(string: "https://httpbin.org/get?data=2") else { return }

let group = DispatchGroup()
group.enter()
AF.request(url1).response{
response in
print("Done MergeApi url1 \(response)")
group.leave()
}

group.enter()
AF.request(url2).response{
response in
print("Done MergeApi url2 \(response)")
group.leave()
}

group.notify(queue: .main) {
print("Done MergeApi")
// TODO: Merge Data or Update UI on main thread.
}
}

情境四:一道一道呼叫API

由於 Alamofire 預設是 async 並行發送,
要做一點處理才能一道API完成後,再呼叫下一道API。

可以利用 group(enter+leave+wait),但切記 wait 不可以在 main thread 上執行不然會卡死,
所以下面範例才使用 DispatchQueue.global().async

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
26
27
28
func handlePipeApi(){
guard let url1 = URL(string: "https://httpbin.org/get?data=1") else { return }
guard let url2 = URL(string: "https://httpbin.org/get?data=2") else { return }

let group = DispatchGroup()
DispatchQueue.global().async {
group.enter()
AF.request(url1).response{
response in
print("Done PipeApi url1 \(response)")
group.leave()
}
group.wait()

group.enter()
AF.request(url2).response{
response in
print("Done PipeApi url2 \(response)")
group.leave()
}
group.wait()

print("Done PipeApi")
DispatchQueue.main.async{
// TODO: Update UI on main thread.
}
}
}

DispatchQueue 應用情境 Demo

Demo - DispatchQueueCase