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

同一时间仅运行一个长时任务:取消旧任务后建新任务是否安全?

同一时间仅运行一个长时任务:取消旧任务后立即创建新任务是否安全?

我需要保证同一时间只运行一个长时轮询任务,现在的疑问是:取消旧任务后立刻创建新任务是否安全?

我写了一段精简后的轮询任务代码:

class SocketClass {
    private var pollingTask: Task<Void, Never>?
    func startPoll() {
        // 停止之前的轮询任务
        pollingTask?.cancel()
        pollingTask = nil

        pollingTask = Task {
            while !Task.isCancelled {
                // 发送ping
                // 等待pong超时
                await self.waitForPong()

                // 等待下一次ping间隔
                try? await Task.sleep(nanoseconds: UInt64(30_000_000_000))
            }
        }
        // 有没有可能到这里时,旧任务还没响应取消,导致两个任务同时在轮询?
    }

    private func waitForPong(_ uuid: UUID, _ count: Int) async -> Bool {
        let pongReceived = await withCheckedContinuation { continuation in
            socket.once("pong") { [weak self] _, _ in
                continuation.resume(returning: true)
            }
        }
        return pongReceived
    }
}

我担心调用startPoll()时,旧轮询任务还没完成取消流程,新任务就已经启动了。我想等待旧任务完全取消后再创建新任务,但不知道该怎么做。

另外,当前的代码是不是没法避免两个轮询任务短暂同时运行的情况?尤其是当ping/pong的代码里没有检查任务是否取消的时候?


问题分析与解决方案

是的,你担心的情况确实会发生。调用pollingTask?.cancel()只是给旧任务发送了取消信号,旧任务不会立刻停止——它只会在检查取消状态或者遇到可取消的挂起点(比如try await Task.sleep)时才会响应取消。如果旧任务正卡在waitForPong里(因为还没收到pong或者续体还没恢复),它根本不会检查Task.isCancelled,这时候新任务已经启动,就会出现两个任务同时运行的情况。

如何等待旧任务取消完成?

要解决这个问题,你需要等待旧任务真正结束后再创建新任务。但注意:直接await pollingTask会阻塞当前线程,而startPoll如果是在主线程调用的,可能会导致UI卡顿,所以建议在后台等待旧任务结束。

修改代码的思路:

  • 取消旧任务后,不要立刻置为nil,而是先等待它完成(可以用try? await忽略取消错误)
  • 等待完成后再创建新任务

修改后的代码示例:

class SocketClass {
    private var pollingTask: Task<Void, Never>?
    func startPoll() {
        // 保存旧任务引用,取消并等待其结束
        let oldTask = pollingTask
        oldTask?.cancel()
        
        // 在后台线程等待旧任务完成,避免阻塞当前线程
        Task {
            _ = await oldTask?.value // 忽略返回值,等待任务彻底结束
            // 校验旧任务是否仍为当前记录的任务,防止多次调用startPoll导致重复创建
            if oldTask === self.pollingTask {
                self.pollingTask = Task {
                    while !Task.isCancelled {
                        // 发送ping
                        // 等待pong超时
                        await self.waitForPong()

                        // 等待下一次ping间隔
                        try? await Task.sleep(nanoseconds: UInt64(30_000_000_000))
                    }
                }
            }
        }
        
        // 立即标记旧任务已被替换,避免后续判断混淆
        pollingTask = nil
    }

    private func waitForPong(_ uuid: UUID, _ count: Int) async -> Bool {
        let pongReceived = await withCheckedContinuation { continuation in
            socket.once("pong") { [weak self] _, _ in
                continuation.resume(returning: true)
            }
            
            // 新增取消监听:任务被取消时主动恢复续体,让旧任务及时退出
            Task {
                await Task.yield() // 等待续体注册完成后再检查取消状态
                if Task.isCancelled {
                    continuation.resume(returning: false)
                }
            }
        }
        return pongReceived
    }
}

关键改进点:

  1. 等待旧任务彻底结束:通过后台Task等待旧任务的value,确保旧任务完全退出后再启动新任务
  2. 防止重复创建任务:用oldTask === self.pollingTask做身份校验,避免startPoll被高频调用时生成多个新任务
  3. 让waitForPong响应取消:在waitForPong中新增后台监听,任务被取消时主动恢复续体,解决旧任务卡在等待pong无法退出的问题

额外注意事项:

  • 如果waitForPong有自定义超时逻辑,也要在超时判断中加入取消状态检查,确保任务能及时响应取消
  • 绝对不要在主线程直接await旧任务,否则会阻塞UI交互,必须放在后台Task中处理

内容的提问来源于stack exchange,提问作者CalebK

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 13:43:16