Swift任务组内任务失败后,其余任务未收到取消状态的问题咨询
当任务组中的任一任务报错失败时,Task的取消状态不会传播。示例中两个长时间异步操作同时启动:第一个耗时3秒并失败,第二个耗时6秒并成功完成。两个任务在完成前都会检查Task.isCancelled状态,但6秒的任务并未收到取消状态。
预期是:一旦第一个任务失败,任务组就会向组内剩余所有任务发送取消指令,6秒的任务应该能检测到Task.isCancelled状态。
模拟长时间异步操作的源码
enum MyError: Error { case generic } func runLongOperation(key: String, sleepSec: UInt32, shouldFailWithError: Bool) async throws -> String { try await withUnsafeThrowingContinuation { continuation in let queue = DispatchQueue(label: "loq") queue.async { print(" \(key) started...") sleep(sleepSec) if Task.isCancelled { print(" ...\(key) Task is cancelled") } if shouldFailWithError { print(" ...\(key) finished with failure") continuation.resume(throwing: MyError.generic) } else { print(" ...\(key) finished with success") continuation.resume(returning: key) } } } }
任务组执行逻辑
func runTasksGroup() async throws -> [String] { return try await withThrowingTaskGroup(of: String.self, body: { group in group.addTask { return try await self.runLongOperation(key: "#1[3s]", sleepSec: 3, shouldFailWithError: true) } group.addTask { return try await self.runLongOperation(key: "#2[6s]", sleepSec: 6, shouldFailWithError: false) } var strs = [String]() for try await str in group { strs += [str] } return strs }) }
启动代码
override func viewDidLoad() { super.viewDidLoad() Task { do { let strs = try await runTasksGroup() print("all done with success \(strs.joined(separator: " "))") } catch { print("some tasks failed") } } }
输出结果
#1[3s] started... #2[6s] started... ...#1[3s] finished with failure ...#2[6s] finished with success some tasks failed
问题:尽管3秒的任务提前失败,但耗时6秒的任务并未收到Task.isCancelled状态,原因是什么?
核心原因分析
Task.isCancelled的上下文错误
你在DispatchQueue.async的闭包里调用Task.isCancelled,但这个闭包运行在GCD队列的线程上,已经脱离了group.addTask创建的异步任务上下文。Task.isCancelled是静态属性,它获取的是当前执行代码所在的Task,而非你创建的那个异步任务,因此这里检测到的取消状态根本不是目标任务的状态。未遵循协作式取消逻辑
Swift的任务取消是协作式的:任务组在第一个任务抛出错误后确实会取消剩余任务,但你的runLongOperation既没有注册取消回调,也没有在任务被取消时中断执行并恢复continuation。另外,你用的sleep()是POSIX阻塞函数,完全无视任务取消信号,会一直阻塞到时间结束。任务组取消的触发时机
withThrowingTaskGroup的for try await循环在收到第一个错误时才会触发任务组取消。当第一个任务抛出错误时,第二个任务已经进入sleep(6)的阻塞状态,这个操作不会被中断,所以它会继续执行到结束。
修正方案
方案1:使用Task.sleep替代sleep()
Task.sleep是异步函数,会响应任务取消,且执行时处于正确的Task上下文:
func runLongOperation(key: String, sleepSec: UInt64, shouldFailWithError: Bool) async throws -> String { print(" \(key) started...") try await Task.sleep(nanoseconds: sleepSec * 1_000_000_000) if Task.isCancelled { print(" ...\(key) Task is cancelled") throw CancellationError() } if shouldFailWithError { print(" ...\(key) finished with failure") throw MyError.generic } else { print(" ...\(key) finished with success") return key } }
方案2:若必须用GCD,手动监听任务取消
如果因业务需求必须使用GCD,需在withUnsafeThrowingContinuation中注册取消回调,并检查原任务的取消状态:
func runLongOperation(key: String, sleepSec: UInt32, shouldFailWithError: Bool) async throws -> String { try await withUnsafeThrowingContinuation { continuation in let currentTask = Task.currentTask! let queue = DispatchQueue(label: "loq") // 注册取消回调,任务取消时恢复continuation let cancellationHandler = currentTask.withCancellationHandler { queue.async { continuation.resume(throwing: CancellationError()) } } queue.async { defer { cancellationHandler.cancel() } // 清理避免内存泄漏 print(" \(key) started...") sleep(sleepSec) // 检查原任务的取消状态,而非当前GCD上下文的状态 if currentTask.isCancelled { print(" ...\(key) Task is cancelled") continuation.resume(throwing: CancellationError()) return } if shouldFailWithError { print(" ...\(key) finished with failure") continuation.resume(throwing: MyError.generic) } else { print(" ...\(key) finished with success") continuation.resume(returning: key) } } } }
修正后的输出
使用方案1后,输出会变为:
#1[3s] started... #2[6s] started... ...#1[3s] finished with failure ...#2[6s] Task is cancelled some tasks failed
内容的提问来源于stack exchange,提问作者Ihar Katkavets

