Swift 并发 2026:async/await、Actor 与 Sendable 严格检查
Swift 5.5 引入 async/await,Swift 6 默认打开严格的 Sendable 检查。2026 年写现代 Swift 已经绕不开这套模型——这是 iOS/服务端 Swift 开发者的工作指南。
关键三件套
- async/await — 让异步代码看起来像同步顺序代码。
- Actor — 保护内部状态不被数据竞争破坏的类型。
- Sendable — 编译器用来证明值能安全跨并发边界的协议。
只用 async/await 能撑一会儿,但只要出现共享可变状态,actor 和 Sendable 立刻登场——Swift 6 严格检查让它们变成必须的而不是可选的。
async/await 5 分钟入门
func loadUser(id: String) async throws -> User { let (data, _) = try await URLSession.shared.data(from: url(for: id)) return try JSONDecoder().decode(User.self, from: data) } // 调用方必须在 async 上下文,或用 Task 包起来 Task { do { let user = try await loadUser(id: "42") } catch { print("failed: \(error)") } }
心智模型:await 是挂起点,函数可以让出控制权、之后被调度回来恢复执行。它不是线程——多个 await 可以在同一个线程上交错执行。
结构化并发 — TaskGroup
要并行十个请求然后汇总:
func loadFriends(_ ids: [String]) async throws -> [User] { try await withThrowingTaskGroup(of: User.self) { group in for id in ids { group.addTask { try await loadUser(id: id) } } var users: [User] = [] for try await user in group { users.append(user) } return users } }
任意一个任务抛错,整组取消——这是"结构化"的含义。没有泄漏的工作,没有孤儿回调。
Actor — 受保护的可变状态
actor Cart { private var items: [Item] = [] func add(_ item: Item) { items.append(item) } func snapshot() -> [Item] { items } } let cart = Cart() Task { await cart.add(.init(id: "1")) let now = await cart.snapshot() }
Actor 把对内部状态的访问串行化。跨 actor 调用必须 await。运行时保证不会两段代码同时改 actor 状态。
@MainActor 是个特殊 actor,把隔离规则绑到主线程——SwiftUI 视图、改 UI 的 view model 都该是 @MainActor。
Swift 6 严格 Sendable
Swift 6 默认开启完整并发检查。任何跨 await 的值,编译器都要确认它是 Sendable——也就是"跨 actor/线程共享是安全的"。
自动 Sendable 的类型:
- 字段全是 Sendable 的值类型(
struct、enum) - 标记
final+Sendable且字段全是let+ Sendable 的类 Actor本身
不是 Sendable 的:
- 可变 class —— 改成 actor 或谨慎标记
- 无约束的泛型类型 —— 加
where T: Sendable
常见迁移痛点:
// ❌ strict 模式报错 final class LegacyImageCache { private var dict: [String: UIImage] = [:] func get(_ k: String) -> UIImage? { dict[k] } } // ✅ 改成 actor actor ImageCache { private var dict: [String: UIImage] = [:] func get(_ k: String) -> UIImage? { dict[k] } }
迁移老回调 API
用 withCheckedThrowingContinuation 包:
func legacyFetch() async throws -> Data { try await withCheckedThrowingContinuation { cont in LegacyAPI.fetch { result in switch result { case .success(let data): cont.resume(returning: data) case .failure(let err): cont.resume(throwing: err) } } } }
铁律:continuation 必须恰好resume 一次。两次会 crash,零次会让等待方永远挂起。
反模式
- 到处
Task { ... }而不是await。 非结构化 Task 让父级失去对它的控制——取消不传播,错误被吞掉。 - async 代码里用
Thread.sleep。 用try await Task.sleep(nanoseconds:)。 @MainActor类里跑重 CPU 循环。 把活儿挪出主 actor,只有结果回到主 actor。- 关掉 Sendable warning 让编译通过。 你在掩盖真实的并发 bug,修它。
一句话总结
- 顺序异步用
async/await;老回调用 continuation 包。 - 并行收集用
TaskGroup,别for { Task { ... } }自求多福。 - 共享可变状态用 actor;UI 用
@MainActor。 - 相信 Swift 6 Sendable 检查——它在抓真 bug。
- 新模型一开始啰嗦,长期结构上更安全,值得。
相关阅读
Objective-C 与 Swift 互操作 2026:什么仍然能用,什么悄悄坏了
2026 年 Objective-C ↔ Swift 互操作的诚实现状 — 桥接头文件、ABI 稳定性、Swift 6 严格并发对老 ObjC 代码的影响、混合代码库求生指南。
Go context 实战:deadline、取消传播与那些救命的模式
Go context 包用法精要 — deadline 与 timeout 的区别、正确传播取消、什么不该放进 context.Value、避免 goroutine 泄漏的模式。
TypeScript 类型体操:什么时候值,什么时候在炫技
务实的 TypeScript 高级类型指南 — mapped types、conditional types、template literal types 真正能给你什么,什么时候用,什么时候应该退回到朴素代码。