You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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状态,原因是什么?


解答

核心原因分析

  1. Task.isCancelled的上下文错误
    你在DispatchQueue.async的闭包里调用Task.isCancelled,但这个闭包运行在GCD队列的线程上,已经脱离了group.addTask创建的异步任务上下文。Task.isCancelled是静态属性,它获取的是当前执行代码所在的Task,而非你创建的那个异步任务,因此这里检测到的取消状态根本不是目标任务的状态。

  2. 未遵循协作式取消逻辑
    Swift的任务取消是协作式的:任务组在第一个任务抛出错误后确实会取消剩余任务,但你的runLongOperation既没有注册取消回调,也没有在任务被取消时中断执行并恢复continuation。另外,你用的sleep()是POSIX阻塞函数,完全无视任务取消信号,会一直阻塞到时间结束。

  3. 任务组取消的触发时机
    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 11:36:15