Nếu đã đọc bài viết trước của mình về task group, mọi người sẽ hiểu bản chất task group, cách khởi tạo, cách add 1 task con, và cách nhận result của tất cả các task con đó.
Tuy nhiên, trong bài viết đó mình không đi sâu vào 1 phần quan trọng của task group, đó là việc handle error.
Nay mình rảnh rảnh nên lên gõ 1 bài về chủ đề này 😁
1-Tổng quát
Như chúng ta đã biết, TaskGroup là 1 tập hợp những task con thực thi đồng thời.
Tuy nhiên, sẽ có vấn đề được đặt ra là khi có bất kỳ 1 trong những task con gặp lỗi, task group handle việc đó như nào? Điều gì sẽ xảy ra cho những task con khác đang thực thi?
Ở bài viết này, mình sẽ đưa ra 2 cách cơ bản để handle error trong một task group, đó là:
– Throw error
– Trả về result của tất cả các task con
Ta sẽ đi sâu vào từng cách ở những phần bên dưới.
2-Throw error
Cách đầu tiên là throw error thông qua withThrowingTaskGroup.
Nhưng trước tiên, chúng ta cần phải hiểu một task group sẽ hoạt động thế nào khi một task con của nó gặp lỗi:
1. Một task group sẽ chỉ throw ra error đầu tiên mà task con của nó gặp phải, task group sau đó sẽ bỏ qua tất cả những error sau. Tức là khi bất kỳ task con nào trong quá trình thực thi mà gặp phải lỗi, task group sẽ throw lỗi đó, và sẽ không quan tâm đến bất kỳ error nào task con của nó gặp phải sau đó nữa.
2. Khi một task con throw 1 error, tất cả các task con còn lại (kể cả những task đang thực thi) sẽ được đánh dấu.
3. Những task con còn lại mà đã được đánh dấu đó sẽ tiếp tục thực thi cho đến khi chúng ta can thiệp và dừng chúng lại.
4. Khi chúng ta đã can thiệp và dừng việc thực thi của những task con còn lại đó, những task con đó sẽ không trigger việc add vào result tổng của 1 task group nữa (kể cả với những task con đã hoàn thành việc thực thi).
Để hiểu rõ hơn, mình có tạo ra 1 struct DemoChildTask, mà trong đó xử lý việc chia 2 số cho nhau, và sẽ throw ra error khi có bất kỳ phép chia nào cho 0
enum ErrorCase: Error {
case divideToZero
}
struct DemoChildTask {
let name: String
let a: Double
let b: Double
let sleepDuration: UInt64
func execute() async throws -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
// Throw error when divisor is zero
guard b != 0 else {
print("\(name) throw error")
throw ErrorCase.divideToZero
}
let result = a/b
print("\(name) has been completed: \(result)")
return result
}
}
Tiếp theo, mình tạo ra 1 task group và add các task con vào trong đó. Các task con đó sẽ thực thi DivideOperation và trả ra 2 giá trị name và result.
Khi tất cả các task con hoàn thành, task group sẽ add các result con đó vào một dictionary result tổng mà trong đó chứa tất cả giá trị name và result của task con.
Bất kể khi nào một trong những task con gặp error, nó sẽ throw và truyền ra cho task group, và task group sẽ throw error đó.
let demoOperation: [DemoChildTask] = [DemoChildTask(name: "operation-0", a: 4, b: 1, sleepDuration: 3),
DemoChildTask(name: "operation-1", a: 5, b: 2, sleepDuration: 1),
DemoChildTask(name: "operation-2", a: 10, b: 5, sleepDuration: 2),
DemoChildTask(name: "operation-3", a: 5, b: 0, sleepDuration: 2),
DemoChildTask(name: "operation-4", a: 4, b: 2, sleepDuration: 5)]
Task {
do {
let allResult = try await withThrowingTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in demoOperation {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = try 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 try 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 return task group result
return childTaskResults
})
print("Task group completed: \(allResult)")
} catch {
print("Task group error: \(error)")
}
}
withThrowingTaskGroup về cơ bản giống với withTaskGroup, tuy nhiên chúng ta cần sử dụng keyword try để có thể throw error. Đồng thời cũng cần dùng try khi call func execute() của task con, điều này cho phép error throw từ execute() có thể truyền ra cho task group. Bên cạnh đó, try cũng phải được dùng khi tổng hợp result từ các task con.
Khi chạy đoạn code trên, ta sẽ nhận được đoạn log như này:
Đoạn output trên cho chúng ta thấy “operation-3” sẽ throw ra lỗi vì đã chia cho 0.
Mặc dù đoạn code trên đã khá hoàn chỉnh cho việc handle error với throw, tuy nhiên để tối ưu hơn, ta sẽ không muốn các task con khác thực thi tiếp khi một task con đã throw ra error. Đây là lúc chúng ta cần phải can thiệp vào việc thực thi của chúng như mình đã note bên trên.
Để làm được việc này, đơn giản có thể sử dụng Task.checkCancellation(). Func này dùng để check những task nào còn đang thực thi.
func execute() async throws -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
// Check for cancellation.
try Task.checkCancellation()
// Throw error when divisor is zero
// guard b != 0 else {
// print("\(name) throw error")
// throw ErrorCase.divideToZero
// }
//
// let result = a/b
// print("\(name) has been completed: \(result )")
// return result
}
Về cơ bản, khi func checkCancellation() gặp bất kỳ task con nào đã được đánh dấu, nó sẽ dừng task con đó và throw ra CancellationError . Tuy nhiên, như mình đã đề cập, việc throw này không mang quá nhiều ý nghĩa vì task group sẽ reject toàn bộ những error này.
Khi chạy lại đoạn code trên với checkCancellation(), ta sẽ nhận được log như này:
3-Trả về result
Cách thứ 2 này thực ra hoàn toàn ngược lại với cách đầu tiên. Cách này cơ bản task group sẽ ignore toàn bộ task con mà throw error, mà sẽ chỉ trả về tất cả result của task con thực thi thành công.
Cách 2 này code khá giống với cách 1, chỉ khác là bây giờ sẽ sử dụng non-throwing task group thay vì throw task group, và sẽ ignore toàn bộ error với try?:
Task {
let allResult = await withTaskGroup(of: (String, Double)?.self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in demoOperation {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
guard let value = try? await operation.execute() else {
return nil
}
// 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.compactMap({ $0 }) {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// All child tasks finish running, thus return task group result
return childTaskResults
})
print("Task group completed: \(allResult)")
}
Để trả về result tổng của tất cả task con, mình sử dụng compactMap để loại bỏ toàn bộ giá trị nil được trả về từ task con.
Khi chạy đoạn code trên, đây sẽ là output:
4-Tổng kết
Như vậy, với 2 bài viết của mình, mọi người đã có những cái nhìn và hiểu biết rõ ràng hơn về task group, một trong những update mới và quan trọng của swift 5.5 😁