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

更新於 發佈於 閱讀時間約 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





留言
avatar-img
留言分享你的想法!
avatar-img
Michelle Chen的沙龍
8會員
34內容數
紀錄iOS開發上遇到的問題或是一些流程筆記。主要都是Swift。
Michelle Chen的沙龍的其他內容
2024/09/06
本文介紹了使用KeyboardLayoutGuide的方法,以及在不同iOS版本和設備上遇到的佈局問題。透過調整TextView的底部對齊方式,成功解決了在模擬器和真實設備上出現的錯誤,提供了有用的建議給開發者。本文還探討了為何在iOS15與Xib的組合使用中會出現問題,以及解決方案。
Thumbnail
2024/09/06
本文介紹了使用KeyboardLayoutGuide的方法,以及在不同iOS版本和設備上遇到的佈局問題。透過調整TextView的底部對齊方式,成功解決了在模擬器和真實設備上出現的錯誤,提供了有用的建議給開發者。本文還探討了為何在iOS15與Xib的組合使用中會出現問題,以及解決方案。
Thumbnail
2024/08/07
使用者回報的超級奇怪線上問題,用數字鍵盤(NumberPad)更改欄位時,送出後尾數都會消失。例如:30 ⭢ 3,52 ⭢ 5。 尋尋覓覓了兩天終於被我找到這篇,apple的奇葩的bug 重現條件 iOS17 手機設定是繁體中文語系 前一個用過的鍵盤是Cangjie倉頡 or Suchen
2024/08/07
使用者回報的超級奇怪線上問題,用數字鍵盤(NumberPad)更改欄位時,送出後尾數都會消失。例如:30 ⭢ 3,52 ⭢ 5。 尋尋覓覓了兩天終於被我找到這篇,apple的奇葩的bug 重現條件 iOS17 手機設定是繁體中文語系 前一個用過的鍵盤是Cangjie倉頡 or Suchen
2024/07/03
這篇文章探討瞭如何在iOS應用程式中客製化Alert,包括改變字體大小、內嵌連結以及讓Alert的高度隨著字數增長並提供scroll操作。同時使用SwiftUI進行客製化,並介紹瞭解決高度超出範圍後文字捲動與scrollView固定高度的方法。
Thumbnail
2024/07/03
這篇文章探討瞭如何在iOS應用程式中客製化Alert,包括改變字體大小、內嵌連結以及讓Alert的高度隨著字數增長並提供scroll操作。同時使用SwiftUI進行客製化,並介紹瞭解決高度超出範圍後文字捲動與scrollView固定高度的方法。
Thumbnail
看更多
你可能也想看
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
「欸!這是在哪裡買的?求連結 🥺」 誰叫你太有品味,一發就讓大家跟著剁手手? 讓你回購再回購的生活好物,是時候該介紹出場了吧! 「開箱你的美好生活」現正召喚各路好物的開箱使者 🤩
Thumbnail
在Python中,queue是一個非常有用的模块。 它提供了多種佇列(queue)實現,用於在多線程環境中安全地交換信息或者數據。 佇列(queue)是一種先進先出(FIFO)的數據結構,允許在佇列的一端插入元素,另一端取出元素。(FIFO 是First In, First Out 的縮寫)
Thumbnail
在Python中,queue是一個非常有用的模块。 它提供了多種佇列(queue)實現,用於在多線程環境中安全地交換信息或者數據。 佇列(queue)是一種先進先出(FIFO)的數據結構,允許在佇列的一端插入元素,另一端取出元素。(FIFO 是First In, First Out 的縮寫)
Thumbnail
當你需要在 Python 中執行多個任務,但又不希望它們相互阻塞時,可以使用 threading 模組。 threading 模組允許你在單個程序中創建多個執行緒,這些執行緒可以同時運行,從而實現並行執行多個任務的效果。
Thumbnail
當你需要在 Python 中執行多個任務,但又不希望它們相互阻塞時,可以使用 threading 模組。 threading 模組允許你在單個程序中創建多個執行緒,這些執行緒可以同時運行,從而實現並行執行多個任務的效果。
Thumbnail
關於多執行緒/多行程的使用方式 在Python 3.2版本之後加入了「concurrent.futures」啟動平行任務, 它可以更好的讓我們管理多執行緒/多行程的應用場景,讓我們在面對這種併發問題時可以不必害怕, 用一個非常簡單的方式就能夠處裡, 底下我們將為您展示一段程式碼: imp
Thumbnail
關於多執行緒/多行程的使用方式 在Python 3.2版本之後加入了「concurrent.futures」啟動平行任務, 它可以更好的讓我們管理多執行緒/多行程的應用場景,讓我們在面對這種併發問題時可以不必害怕, 用一個非常簡單的方式就能夠處裡, 底下我們將為您展示一段程式碼: imp
Thumbnail
這個浮誇標題是ChatGPT幫我想的😊 圖也是DALL幫我產的, AI萬歲! GCD也是面試必問題啊!我答超爛的,只會用背景呼叫API+切Main Thread更新畫面,今天就來認真了解。 GCD是什麼? 先來問ChatGPT什麼是GCD? GCD,全名 Grand Central Dis
Thumbnail
這個浮誇標題是ChatGPT幫我想的😊 圖也是DALL幫我產的, AI萬歲! GCD也是面試必問題啊!我答超爛的,只會用背景呼叫API+切Main Thread更新畫面,今天就來認真了解。 GCD是什麼? 先來問ChatGPT什麼是GCD? GCD,全名 Grand Central Dis
Thumbnail
👨‍💻簡介 昨天講到Goroutine的橋梁aka傳話筒 — Channel,那要怎麼知道對方有收到訊息,我的紙條有送到對方手上呢? 今天就是要來介紹幾種Goroutine的確定完成工作的幾種方式。
Thumbnail
👨‍💻簡介 昨天講到Goroutine的橋梁aka傳話筒 — Channel,那要怎麼知道對方有收到訊息,我的紙條有送到對方手上呢? 今天就是要來介紹幾種Goroutine的確定完成工作的幾種方式。
Thumbnail
👨‍💻簡介 在日常生活中,如果能同時做很多事情,效率肯定大大提升,那麼在Go語言中,該如何做到呢,答案就是今天的主角Goroutine了,在Go語言中,讓併發變得簡單的強大工具,今天就是來給他一個快速介紹。
Thumbnail
👨‍💻簡介 在日常生活中,如果能同時做很多事情,效率肯定大大提升,那麼在Go語言中,該如何做到呢,答案就是今天的主角Goroutine了,在Go語言中,讓併發變得簡單的強大工具,今天就是來給他一個快速介紹。
Thumbnail
  在說執行緒(Thread)時就要先知道什麼是程式(Program)、程序(Process),才能了解什麼是執行緒(Thread),因為它們間都有著神秘的關係與關聯,再更深入一點就又會有多程序(muti-Process)、多執行緒(muti-Thread),我們就先一一說明好了,了解它們後再使用時
Thumbnail
  在說執行緒(Thread)時就要先知道什麼是程式(Program)、程序(Process),才能了解什麼是執行緒(Thread),因為它們間都有著神秘的關係與關聯,再更深入一點就又會有多程序(muti-Process)、多執行緒(muti-Thread),我們就先一一說明好了,了解它們後再使用時
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News