同一时间仅运行一个长时任务:取消旧任务后建新任务是否安全?
同一时间仅运行一个长时任务:取消旧任务后立即创建新任务是否安全?
我需要保证同一时间只运行一个长时轮询任务,现在的疑问是:取消旧任务后立刻创建新任务是否安全?
我写了一段精简后的轮询任务代码:
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 } }
关键改进点:
- 等待旧任务彻底结束:通过后台Task等待旧任务的
value,确保旧任务完全退出后再启动新任务 - 防止重复创建任务:用
oldTask === self.pollingTask做身份校验,避免startPoll被高频调用时生成多个新任务 - 让waitForPong响应取消:在
waitForPong中新增后台监听,任务被取消时主动恢复续体,解决旧任务卡在等待pong无法退出的问题
额外注意事项:
- 如果
waitForPong有自定义超时逻辑,也要在超时判断中加入取消状态检查,确保任务能及时响应取消 - 绝对不要在主线程直接
await旧任务,否则会阻塞UI交互,必须放在后台Task中处理
内容的提问来源于stack exchange,提问作者CalebK
相关产品推荐
相关产品推荐

