Swift 5.5 的 Task Group 讓我們快速地建立子任務並收集結果


本篇原文(標題:Understanding Swift 5.5 Task Groups)刊登於作者 Medium,由 Lee Kah Seng 所著,並授權翻譯及轉載。

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 的特性:

  1. 一個 Task Group 是由一組獨立而非同步的任務(子任務)組成的。
  2. Task Group 內所有子任務都會自動並行執行。
  3. 我們無法控制子任務完成的時間。因此,如果我們想子任務依特定次序完成,就不應該使用 Task Group
  4. Task Group 只會在所有子任務完成後才會回傳結果。換句話說,所有子任務只能存在於 Task Group 內。
  5. 最後,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)。它在被呼叫時會是這樣的:

Calling withTaskGroup(of:returning:body:)

在這個範例中,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 {{EJS0}} 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) 匹配。

Matching child task return 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 結果型別匹配。

Matching task group result type

小竅門:

如果你有回傳 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。

本篇原文(標題:Understanding Swift 5.5 Task Groups)刊登於作者 Medium,由 Lee Kah Seng 所著,並授權翻譯及轉載。

作者簡介Lee Kah Seng,從 2011 年開始做 iOS 開發,喜歡 Swift、音樂、和動漫,也是一個兼職背包客。你可以在這裡看看我其他的文章。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This