GCD 神秘面紗揭開:讓你的 Swift 應用更強大

2023/10/30閱讀時間約 16 分鐘
GCD image by DALL·E 3

GCD image by DALL·E 3

這個浮誇標題是ChatGPT幫我想的😊 圖也是DALL幫我產的, AI萬歲!

GCD也是面試必問題啊!我答超爛的,只會用背景呼叫API+切Main Thread更新畫面,今天就來認真了解。

GCD是什麼?

先來問ChatGPT什麼是GCD?

GCD,全名 Grand Central Dispatch,是 Apple 提供的一種用於管理並行執行任務的技術。它是一種低階的 C API,用於優化應用程序以便在多核硬體上更有效地並行運行。

GCD 提供了一種簡單的方式來管理並行任務,而不需要直接管理線程的生命週期。它允許你將任務(也稱為“塊”或“閉包”)提交到不同的隊列(Queue)中進行執行。這些隊列可以是串行的(Serial,一次只執行一個任務)或並行的(Concurrent,可同時執行多個任務)。

簡單來說就是接受任務的queue。比較輕量級,簡單快速。

DispatchQueue

  1. FIFO(先到先執行)
  2. serially or concurrently
  3. 創造太多的Queue可能會造成Thread Explosion

基本用法

  • main: 調度Main Thread。
  • global: 全域的queue,可以用qos來調整執行的優先度。

把Code直接寫在block裡執行,不能等待也不能取消

DispatchQueue.global().async {    
// 一些比較花時間的操作
DispatchQueue.main.async {
// update UI
}
}

客製化的Queue

開發者自己創造的queue,最好是給他一個Label來識別(名稱寫法:com.xxx.xxxxx),因爲app裡會產生很多queue。預設是一次只執行一個任務,可以用attributes: .concurrent改成多個並行。

DispatchQueue(label: "com.custom.queue", attributes: .concurrent).async {
// 看你要做什麼
}

優先執行順序QoS

userInitiate > default > utility > background > unspecified

DispatchQueue.global(qos: .userInitiate).async {    
// 看你要做什麼
DispatchQueue.main.async {
// update UI
}
}

避免Thread Explosion

GCD有一個需要注意的點,為了要完成所有的任務,他有可能會一直創造Thread,最後造成App耗盡所有的Thread跟資源。通常發生在下列兩種狀況:第一,某個在cocurrent queue上的任務擋住現在的Thread,GCD會在創造另一個Thread去執行其他的任務。第二,創造太多custom queue,每個queue也都會創造自己的Thread。建議沒有特殊需求用global queue即可。

萬惡的Deadlock

這也是面試很愛考的。Deadlock是指雙方都互相在等對方,沒有一方先退出,造成死結。

官網上也簡單明暸一句話,在main queue上同步執行task會造成deadlock。

Attempting to synchronously execute a work item on the main queue results in deadlock.

在Xcode上試試看,viewDidLoad裡寫DispatchQueue.main.sync 會crash。

這邊crash的原因,我想是因為在main thread裡又用print同步組塞了main thread,app就直接crash了。

DispatchQueue.main.sync

DispatchQueue.main.sync

非同步裡再同步呼叫Main Thread編譯通過。

DispatchQueue.global().async {
print("1")
DispatchQueue.main.sync {
print("2")
}
print("3")
}

// console output:
// 1
// 2
// 3


第二種情境

測試Serial Queue的各種sync/async組合會不會出錯

Case 3 原理上會造成Deadlock,在async區塊內,有一個sync區塊被分派到同一個Serial Queue裡。裡面的sync block在等外面的async執行完,外面的async無法完成因為他也在等裡面的sync執行完,不過文章裡說只會print出1的結果跟我實測不太一樣,我這邊實測也是直接crash。

crash!!

crash!!

async {} 裡包 sync {} 必定crash?

測到這邊就讓我想到為什麼上面DispatchQueue.global().async裡包DispatchQueue.main.sync就不會crash,serial.async { }裡包一個sync{ }就會crash?關鍵應該是在第一種其實是兩個不同Thread,本來就不會互相塞住,但第二種是加在同一個Serial Queue裡,會互相干擾。

如何避免Deadlock?

不要造成互等的狀況。上面那段程式碼可以改成async非同步,彼此不互等。

let serial = DispatchQueue(label: "first")

serial.async {
print("1")

serial.async {
print("2")
}

print("3")
}

// console output:
// 1
// 3
// 2


DispatchWorkItem

DispatchWorkItem 是一種封裝 GCD 任務的方式。就是把程式碼包成一個task的感覺,再提交到queue來執行。他很有用的是他提供取消跟等待的操作,比直接把code寫在DisaptchQueue裡的Block又更有靈活度。

創造任務

let workItem = DispatchWorkItem {
print("Work item is running.")
}

直接執行

在當下的Thread直接同步執行。

workItem.perform()

提交任務

不直接執行,提交到特定的queue來執行。

// 將工作項提交到後台隊列
DispatchQueue.global().async(execute: workItem)

取消任務

沒辦法取消正在進行的任務,只能取消還沒執行的任務。

workItem.cancel()

等待任務完成

同步的等待任務執行完。

workItem.wait()

例如:

用一個非同步的queue去執行,順序可能會掉換,看哪個先做到

let workItem = DispatchWorkItem { 
print("Work item is running.")
}
DispatchQueue.global().async(execute: workItem)
print("123456")

// console output:
// 123456
// Work item is running.

// or
// Work item is running.​
// 123456

加入wait後,123456就一定會在workItem執行完後才跑

DispatchQueue.global().async(execute: workItem)
workItem.wait()
print("123456")

// console output:
// Work item is running.​
// 123456

增加順序

等待一個任務完成,再執行下一個。

workItem.notify(queue: DispatchQueue.global(), execute: workItem2)

DispatchGroup

DispatchGroup 是 Swift 中的一種結構,它是 Grand Central Dispatch (GCD) 的一部分,用於追蹤一組任務的完成情況。你可以把多個任務添加到一個 DispatchGroup,然後等待他們全部完成,或者在他們全部完成時獲得通知。

我覺得這個用在可以把一堆任務綁成同一個群組,workItem是用在少量任務的情況,Group就用在執行多個任務。DispatchGroup也有通知/等待等操作可以使用。

創造一個Group,進queue執行

let group = DispatchGroup()
// 把任務寫在queue的block中​
DispatchQueue.global(qos: .userInitiated).async(group: group) {
print("Task 1 started")
// 做一些工作...
print("Task 1 finished")
}


enter/ leave: 標示group進入與離開

手動標示一個任務加入group與完成。管理group任務的技術,背後是dispatch_group_enterdispatch_group_leave ,兩個需要配對。

group.enter()
group.leave()
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

// 進入Task1​​
group.enter()
queue.async(group: group) {
print("Task 1 started")
print("Task 1 finished")
group.leave() // 執行完離開
}
// 進入Task2
group.enter()
queue.async(group: group) {
print("Task 2 started")
print("Task 2 finished")
group.leave() // 執行完離開
}

group.notify(queue: DispatchQueue.main) {
print("All tasks are done!")
}

Notify: 異步等待Group內任務全部執行完,發出通知

group.notify(queue: YOUR_QUEUE) {
print("All tasks are done!")
}

例如:

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

// Task1​
queue.async(group: group) {
print("Task 1 started")
print("Task 1 finished")
print(Thread.current)
}

// Task2
queue.async(group: group) {
print("Task 2 started")
print("Task 2 finished")
print(Thread.current)
}

group.notify(queue: DispatchQueue.main) {
print("All tasks are done!")
}

// console output:
// Task 1 started
// Task 2 started
// Task 2 finished
// <NSThread: 0x600001ff8200>{number = 5, name = (null)}
// Task 1 finished
// <NSThread: 0x600001ffcc80>{number = 6, name = (null)}
// All tasks are done!​

不過我比較不懂要前面用enter/leave手動管理的意義,因為不寫的結果也是一樣的?

自問自答:如果queue.async{}的裡也是非同步的任務才有差別。

Wait: 同步等待前一個任務執行完

group.wait()

上面notify那段也可以改成

// ....做完Task​1 & 2
group.wait()
print("All tasks are done! 2")

// console output:
// Task 1 started
// Task 2 started
// Task 2 finished
// <NSThread: 0x600001ff8200>{number = 4, name = (null)}
// Task 1 finished
// <NSThread: 0x600001ffcc80>{number = 5, name = (null)}
// All tasks are done! 2!​
用wait要小心,因為notify是異步(async)通知不會阻塞Thread,wait是同步(sync)把Thread停住。

DispatchSemaphore

DispatchSemaphore 是 Swift 中用於控制並行執行的同步機制。它能夠限制並行任務的數量,常用於解決資源競爭或同步執行的情況。透過 DispatchSemaphore,你可以設定同時進入臨界區域的執行緒或任務數量,確保資源在同一時間只有特定數量的執行緒可以訪問,避免並行執行時出現的問題,例如資源競爭或資料不一致性。

init(value:)

Value傳入計數器的初始值,一開始不能小於0。

將值設為0對於兩個thread需要協調特定事件完成時很有用。將值設為大於0可以控制資源的使用。

Signal/wait

必須是一對的。

  • Signal: 任務完成後呼叫,計數器+1。如果前一個值是負數,這個function會叫醒在wait的Thread。
  • Wait:等待任務完成,計數器-1。如果此時為負數,他會等待signal發生。
func processTask(completion: (() -> Void)) {
print("Start processing some task......")
sleep(3)
completion()
}

func reloadUI() {
print("Start reload UI.")
}

// ...
let semaphore = DispatchSemaphore(value: 0)

processTask {
print("Task completed.")
semaphore.signal() // 任務完成
}

semaphore.wait() // 等待任務完成後,更新UI
reloadUI()

// console output:
// Start processing some task......
// Task completed.
// Start reload UI.​


參考資料:

https://blog.csdn.net/zhangmengleiblog/article/details/108365032

https://medium.com/%E7%A8%8B%E5%BC%8F%E6%84%9B%E5%A5%BD%E8%80%85/ios-gcd-dispatchgroup-dispatchsemaphore%E4%BB%8B%E7%B4%B9-ab4182c61dc5





5會員
22內容數
紀錄iOS開發上遇到的問題或是一些流程筆記。主要都是Swift。
留言0
查看全部
發表第一個留言支持創作者!