并发常见陷阱与最佳实践
本教程共 93 篇 · 第 87 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:识别 Swift 并发里最易踩的坑,并掌握对应的写法习惯,让并发代码既安全又流畅。
前面几章把并发的语法一件件拆开了讲。这一章做收口:把初学者最容易栽进去的坑集中亮出来,再配上经过验证的最佳实践。把这一章记住,你写并发代码时能少掉一半 bug。
1-1 陷阱一:忘了写 await,或写错地方
await 不是装饰,它是挂起点的标记。最常见的编译错误就是”这里调用了异步函数却没写 await”。更隐蔽的是 try 和 await 的遗漏:既抛错又异步的函数,两个都得写。
// 错误:漏了 try 和 await
let data = listPhotos(inGallery: "x")
// 正确
let data = try await listPhotos(inGallery: "x")
实践:看到异步函数调用就本能确认 await(和必要的 try)在场。编译器报错”call is ‘async’""call can throw”时,第一时间补这两个词。
1-2 陷阱二:同步代码里直接调异步
这是迁移老代码时最痛的一点:你没法在同步函数里写 await,也没法”等”一个异步结果回来。硬要写会被编译拒绝。
func bad() -> String {
// return await fetchName() // 编译错误:同步上下文不能 await
}
实践:自顶向下改。先让最外层函数变成 async(或放进 Task {}),再逐层把被它调用的函数也改成异步。不要试图在底层”包一层”把异步变同步——Swift 故意不支持,那会引入死锁和竞争。
1-3 陷阱三:长任务不检查取消
你以为 task.cancel() 一调用任务就没了,结果它一路跑完。原因在第 82 章讲过:取消是协作式的,任务体里必须有取消检查才会停。
// 危险:从不检查取消,cancel() 等于没用
func work() async {
for i in 0..<1_000_000 {
doExpensiveStep(i) // 永远跑到头
}
}
// 安全:周期性检查
func workSafe() async throws {
for i in 0..<1_000_000 {
try Task.checkCancellation()
doExpensiveStep(i)
}
}
实践:循环、批量处理、网络等待等长任务里,埋入 try Task.checkCancellation() 或 Task.isCancelled 检查。需要清理用 isCancelled,直接放弃用 checkCancellation()。
1-4 陷阱四:跨并发域传了不可发送的数据
把含可变状态的普通类实例,从一个任务传给另一个任务或 actor,编译期就会报”类型不 Sendable”。这是好事——它在拦数据竞争。
class MutableBox { // 不可发送:有可变状态又无保护
var value = 0
}
actor Logger {
func log(_ box: MutableBox) { } // 编译错误:MutableBox 不是 Sendable
}
实践:要跨域传递的数据,优先用纯值类型(结构体/枚举),自动可发送;必须用类时,做成全只读(let 属性)或自带隔离(如标 @MainActor)。别为了通过编译就谎标 : Sendable。
1-5 陷阱五:在后台线程更新了 UI
界面更新没跑在主 actor 上,后果是偶发卡顿、偶发崩溃,报错指向 UI API。根因是你在一个没标 @MainActor 的异步函数里直接改了界面状态。
func load() async {
let img = await download() // 后台
imageView.image = img // 危险:没在主 actor 上
}
// 正确:切回主 actor 再更新
func loadSafe() async {
let img = await download()
await MainActor.run { imageView.image = img }
}
实践:所有读写界面状态的代码,确保位于 @MainActor 标记的上下文。耗时计算留在后台,最后一步 await 切回主 actor 更新。也可以用 MainActor.run { } 临时切回。
Warning别把长耗时逻辑也圈进
@MainActor,那等于把并发全串行回主线程,界面照样卡。精准区分”耗时”和”更新”:前者后台,后者主 actor。
1-6 陷阱六:用 actor 却忘了 await
访问 actor 的属性或方法必须写 await,因为它要排队跨边界。漏写直接编译错。还有人误以为 actor 内部也到处要 await——其实 actor 自己的同步方法里直接访问,不写 await。
实践:跨 actor 边界(从外部访问)一律 await;actor 内部的同步逻辑自然数直连,保持无挂起点以杜绝中途被打断。
1-7 最佳实践清单
把下面几条当习惯刻进肌肉记忆:
一、优先用值类型。结构体、枚举天然值语义,易推理、可发送,配合 let 更稳。
二、优先用结构化并发(async let、任务组),少用非结构化 Task {},尽量不用 Task.detached。
三、共享可变状态就封装进 actor,别自己用锁。actor 隔离在编译期强制,比锁清晰。
四、长任务必埋取消检查,让取消真正生效。
五、UI 全在主 actor,耗时全在后台,最后一步切回更新。
六、跨域数据用 Sendable,宁可用值类型也不要谎标协议。
七、用 for await 处理流式数据,别硬攒大数组。
Tip一个快速自检法:写完整段并发代码,问自己三个问题——“哪些会并行?“(看 async let/任务组)、“会不会被取消?“(看取消检查)、“共享数据谁保护?“(看 actor/Sendable)。三个都答得上来,基本就稳了。
1-8 关于版本的提醒
本章所有语法(async/await、actor、Sendable、@MainActor、AsyncSequence、结构化并发)在 Swift 5.5 起陆续稳定,到 6.3.2 已完全可用,按稳定写法讲解即可。你可能会在网上看到一些更新的并发特性(如某些 6.4 beta 才出现的细化能力),它们需要显式标注「(6.4 预览)」才可使用。本教程基线为 6.3.2,未标注预览的特性都应视为已稳定,放心使用。
1-9 一个完整的小例子串起来
把前面所有工具拼到一个小场景里:并发下载多张图、更新进度、可被取消、UI 在主 actor。伪代码长这样:
@MainActor
func updateProgress(_ value: Double) {
progressBar.progress = Float(value) // 主 actor 上更新界面
}
func downloadAll(_ urls: [URL]) async {
await withThrowingTaskGroup(of: Void.self) { group in
for (i, url) in urls.enumerated() {
group.addTask {
let data = try await fetch(url) // 后台下载
try Task.checkCancellation() // 允许取消
await updateProgress(Double(i) / Double(urls.count))
save(data)
}
}
}
}
这里 withThrowingTaskGroup 并行下载;Task.checkCancellation() 让取消生效;updateProgress 标 @MainActor 保证界面更新在主线程;fetch 是普通异步函数。一个片段就把”并行、取消、隔离、UI 安全”全展示了——这正是现代 Swift 并发的惯用形态。
Tip读到这如果你觉得”概念好多”,别慌。真实项目里它们是一套组合拳:结构化并发管并行、actor/Sendable 管共享、主 actor 管 UI、取消管退出。先照着模板写,写着写着就顺了。
1-10 调试并发问题的几个抓手
最后给点调试方向的建议,真出问题时不至于懵。
第一,善用 deinit 打印。怀疑对象没释放(循环引用),就在 deinit 里打日志,看该释放时释放了没。
第二,关注编译器关于 Sendable 和 actor 隔离的报错,别急着用 unsafe 或强转绕开——那往往是设计有问题的信号,不是编译器的刁难。
第三,怀疑 UI 在非主线程更新时,把相关代码临时包进 MainActor.run 或用断点确认调用栈是否在主线程。
第四,取消不生效,先检查长任务里有没有 Task.checkCancellation() 或 Task.isCancelled。绝大多数”取消没反应”都是漏了检查点。
Warning并发 bug 往往”偶尔出现、难以复现”。不要因为某次没崩就以为没问题,编译器的并发检查(Sendable、隔离)才是你能依赖的硬保障。把代码写得让编译器满意,比靠测试碰运气靠谱。
1-11 小结:模块十收官
并发的本质,是让程序在等待时不被卡死,同时避免多段代码抢同一份数据。Swift 用一套语言级方案把安全前置到编译期:async/await 写清晰异步代码、结构化并发管生命周期、actor 与 Sendable 防数据竞争、主 actor 守界面、AsyncSequence 流式消费。
避开本章列的六个坑、守住七条实践,你就能写出既快又稳的并发代码。到这一章,模块十”并发编程”全部讲完,Swift 语言核心(语法、类型、面向对象、内存、并发)也已覆盖完整。