Apple 在 Swift 5.5 中新增了 Task Group,它在 Swift 並行框架 (concurrency framework) 中非常重要。一如其名,Task Group 集合了一組並行執行的子任務,在所有子任務 (child task) 執行完成後才會回傳結果。
在這篇教學文章中,我將會帶大家建立 task group、把子任務添加到 Task Group、以及從所有子任務收集結果。文章的內容非常多,事不宜遲,讓我們開始吧!
準備工作
一如以往,我會利用一些範例程式碼作說明,讓你可你了建 Task Group 如何在 Swift 中操作。在深入了解 Task Group 之前,讓我們先定義一個在整篇文章都會用到的操作結構 (operation struct)。
struct SlowDivideOperation {
let name: String
let a: Double
let b: Double
let sleepDuration: UInt64
func execute() async -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
let value = a / b
return value
}
}
SlowDivideOperation
是一個簡單的結構,它包含了一個 execute()
函式,可以對指定的數字執行除法運算 (divide operation)。你會注意到,我故意在它執行運算前進行睡眠,以減慢執行速度。
這個做法可以讓我們掌握好除法運算所執行的時間,如此一來,我們就更容易觀察到在同一個 Task Group 內,並行執行多個 SlowDivideOperation
的實際情況。
接下來,我們就可以看看 Task Group 了。
Task Group 的特性 (Behavior)
要正確地使用 Task Group,我們需要注意以下幾個 Task Group 的特性:
- 一個 Task Group 是由一組獨立而非同步的任務(子任務)組成的。
- Task Group 內所有子任務都會自動並行執行。
- 我們無法控制子任務完成的時間。因此,如果我們想子任務依特定次序完成,就不應該使用 Task Group。
- Task Group 只會在所有子任務完成後才會回傳結果。換句話說,所有子任務只能存在於 Task Group 內。
- 最後,Task Group 可能回傳一個數值、或一個 Void(沒有回傳數值)、又或是拋出一個錯誤。
了解 Task Group 的特性後,我們就可以開始編寫程式碼了!
建立一個 Task Group
我們可以利用 Swift 5.5 新增的 withTaskGroup(of:returning:body:)
或是 withThrowingTaskGroup(of:returning:body:)
函式,來建立一個 Task Group。因為我們不想構建的 Task Group 會拋出錯誤,我們會使用範例程式碼內的 withTaskGroup(of:returning:body:)
變形 (variant)。它在被呼叫時會是這樣的:
在這個範例中,Task Group 內會有多個子任務,而子任務會執行 SlowDivideOperation
,並回傳其名稱和結果。當所有 SlowDivideOperation
完成後,Task Group 就會收集所有子任務的結果,並回傳一個 Dictionary,當中包含了所有 SlowDivideOperation
的名稱和結果。
了解這個概念之後,我們就可以利用函數如此建立一個 Task Group:
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// We can use `taskGroup` to spawn child tasks here.
})
你可以看到,body
閉包的參數是 Task Group 的實例 (instance)。接下來,我們會利用這個 Task Group 來建立多個並行執行的子任務。
Task Group 的實際操作
讓我們先建立一個 SlowDivideOperation
陣列:
let operations = [
SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]
然後,我們就可以 loop through operations
陣列,在 Task Group 添加子任務:
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in operations {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = await operation.execute()
// Return child task result
return (operation.name, value)
}
}
// Collect child task results here...
})
請注意,我們在子任務執行的閉包中,回傳了 String
和 Double
元組 (tuple),這與我們之前設置的子任務結果資料型別 (data type) 匹配。
如前文所述,所有子任務都會並行執行,我們無法控制它們完成運作的時間。因此,我們需要這樣 loop through Task Group,來收集子任務的結果。
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for await result in taskGroup {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// Task group finish running & return task group result
return childTaskResults
請注意,我們在 Loop 中使用了 await
關鍵字,也就是說 for
loop 會暫停運作,來等待子任務完成後的結果。每次當子任務回傳結果時,for
loop 就會迭代 (iterate),並更新 childTaskResults
dictionary。
所有子任務完成後,for
loop 就會退出,並回傳 Task Group 的結果。你可以看到,childTaskResults
的資料型別一定會與我們之前設定的 Task Group 結果型別匹配。
小竅門:
如果你有回傳 Void 的 Task Group 或子任務,可以使用
Void.self
結果型別。
在運行範例程式碼之前,讓我們在觸發 Task Group 前後先加入 print 語句,來檢驗最終結果和執行所有子任務所需的時間。以下是完整的範例程式碼:
let operations = [
SlowDivideOperation(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
SlowDivideOperation(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
SlowDivideOperation(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
]
Task {
print("Task start : \(Date())")
let allResults = await withTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in operations {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = await operation.execute()
// Return child task result
return (operation.name, value)
}
}
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for await result in taskGroup {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// All child tasks finish running, thus task group result
return childTaskResults
})
print("Task end : \(Date())")
print("allResults : \(allResults)")
}
讓我們執行以上的程式碼,就會得到以下的結果:
Task start : 2021-10-23 05:53:15 +0000
Task end : 2021-10-23 05:53:20 +0000
allResults : ["operation-1": 2.0, "operation-2": 4.0, "operation-0": 5.0]
如你所見,整個 Task Group 用了 5 秒來完成。從上面的程式碼可見,SlowDivideOperation
中最長的睡眠時間也是 5 秒,證明所有子任務都真的是並行執行的。
allResults
裡面包含了所有子任務的結果,這證明了 Task Group 只會在所有子任務執行完成後才會回傳結果,同時也代表子任務只能存在於 Task Group 的 Context 中。
你可以在 GitHub 下載這篇文章的範例程式碼。
總結
在這篇文章中,我教了大家建立 Task Group、把子任務添加到 Task Group、並從 Task Group 的子任務中收集結果。在下一篇文章,我會教大家處理 Task Group 內的錯誤。
你可以在 Twitter 追蹤我,那就不會錯過我有關 iOS 開發的文章了。
謝謝你的閱讀。
特別鳴謝 Anupam Chugh。