Swift 并发中的取消:让 Task 真正停下来
Task.cancel() 只是发出协作式取消信号。本文从检查点、错误传播和底层请求清理三个层面,整理一套可维护的取消方案。
在 Swift Concurrency 中,取消不是强制终止。调用 task.cancel() 后,任务仍然可能继续执行,直到代码主动检查取消状态、某个异步 API 响应取消,或者任务自然结束。
这是一种协作式模型。它避免任务在任意一条指令上被突然中断,却也意味着取消逻辑必须作为正常控制流来设计。
cancel() 实际做了什么
下面的调用只会把任务标记为已取消:
let task = Task {
await loadTimeline()
}
task.cancel()
如果 loadTimeline() 内部没有取消检查,而且调用的异步函数也不响应取消,这段工作依然会继续。可以把取消信号理解为一个由并发运行时维护的状态,而不是线程中断。
常用的检查方式有两个:
// 只读取状态,不抛出错误
if Task.isCancelled {
return
}
// 已取消时抛出 CancellationError
try Task.checkCancellation()
当函数本身已经是 throws 时,优先使用 Task.checkCancellation(),这样取消可以沿着调用链自然传播。
把检查点放在有意义的位置
不需要在每一行代码前检查取消。合适的位置通常是:
- 即将开始一段昂贵计算之前;
- 循环处理一批数据的边界处;
- 一个异步阶段完成、准备进入下一阶段时;
- 准备提交 UI 或缓存结果之前。
func buildThumbnails(for images: [UIImage]) async throws -> [UIImage] {
var thumbnails: [UIImage] = []
thumbnails.reserveCapacity(images.count)
for image in images {
try Task.checkCancellation()
thumbnails.append(resize(image))
await Task.yield()
}
return thumbnails
}
Task.yield() 给其他任务一次调度机会,但它不是取消检查的替代品。真正决定函数是否结束的,仍然是显式检查或会响应取消的异步调用。
清理底层资源
有些旧式 API 通过回调工作,并不知道外层 Swift Task 已经取消。此时可以用取消处理器,把外层信号转交给底层对象。
func fetchData(using request: NetworkRequest) async throws -> Data {
try await withTaskCancellationHandler {
try await request.value()
} onCancel: {
request.cancel()
}
}
onCancel 可能和操作并发发生,因此里面应只调用线程安全、可重复执行的清理方法。不要在这里更新 UIKit,也不要假设操作闭包已经走到某一个固定阶段。
避免吞掉取消错误
一个常见问题是把所有错误都转成空结果:
do {
return try await loadContent()
} catch {
return []
}
这样会把取消伪装成“加载成功但没有内容”。上层既无法区分状态,也可能继续执行缓存或界面更新。更合适的做法是保留取消语义:
do {
return try await loadContent()
} catch is CancellationError {
throw CancellationError()
} catch {
logger.error("Load failed: \(error)")
throw error
}
与界面生命周期配合
界面层通常持有一个可取消任务,并在新请求开始或页面离开时取消旧任务。
@MainActor
final class TimelineViewModel: ObservableObject {
private var loadTask: Task<Void, Never>?
func reload() {
loadTask?.cancel()
loadTask = Task { [weak self] in
do {
let value = try await repository.load()
try Task.checkCancellation()
self?.items = value
} catch is CancellationError {
// 预期控制流,不展示错误提示
} catch {
self?.error = error
}
}
}
deinit {
loadTask?.cancel()
}
}
最后一次取消检查很重要:网络请求结束到更新状态之间,任务仍然可能被取消。没有这一步,旧请求就可能覆盖新页面的数据。
一份实用检查清单
- 明确谁创建任务,谁负责取消任务;
- 在耗时阶段之间加入取消检查;
- 将取消信号传递到底层网络、文件或数据库操作;
- 不把
CancellationError当成普通失败展示给用户; - 提交界面与缓存结果前再次检查;
- 为快速连续刷新、页面退出等场景写测试。
取消不是异常收尾,而是并发任务的一条正常结束路径。把它纳入 API 设计后,页面切换、搜索联想和重复刷新都会更可预测。