Swift 并发:幕后机制
Swift concurrency: Behind the scenes
2021年6月9日
一句话判断
这场 Session 解开了 Swift 并发模型的底层实现——Continuation、cooperative thread pool、actor isolation 的工作原理都在这里,看完你才能真正理解为什么 async 函数不会阻塞线程。
这场 Session 讲了什么
Session 从底层实现的角度讲解 Swift 并发模型。不是教你怎么用 async/await,而是解释编译器和运行时怎么协作让 async/await 工作。
核心概念包括:Swift 的并发线程池(Cooperative Thread Pool)是一个固定大小的线程池,线程数量等于 CPU 核心数。async 函数在 suspend point 处”挂起”时,线程不会被阻塞而是被释放回线程池供其他任务使用。当异步操作完成后,任务被重新调度到某个线程上继续执行。
还详细讲解了 Continuation 的实现——async 函数的局部变量在 suspend 时被保存到堆上的 Continuation 结构中,resume 时恢复。Actor 的隔离通过编译时检查和运行时的 serial executor 实现。Task 的取消通过 Cooperative Cancellation 实现——系统不会强制取消你的任务,你需要定期检查 Task.isCancelled。
值得深挖的点
为什么线程不会耗尽
GCD 的模型是”一个队列上的任务会阻塞一个线程”。如果你有 100 个并发网络请求,在等待响应时 100 个线程都被阻塞。Swift 的并发模型通过 suspend/resume 机制避免了这个问题——所有等待中的 async 函数不占用线程,它们的状态保存在堆上的 Continuation 中。线程池中永远只有 CPU 核心数个线程在活跃工作。这就是为什么 Swift 并发可以轻松处理上万个并发任务而不会耗尽线程。
Actor 的 Serial Executor
Actor 保证内部状态不被并发访问。实现方式是每个 Actor 有一个关联的 Serial Executor——所有对 Actor 方法的调用都被排队执行。当你 await actor.doSomething() 时,如果 Actor 正在忙,你的任务会挂起并加入等待队列。Actor 完成当前任务后,从队列中取下一个执行。这就是为什么 Actor 方法的调用总是需要 await——编译器不知道调用时 Actor 是否空闲。
代码片段
理解 suspend point 的行为
func fetchUserData() async -> User {
// 此处线程 A 正在执行
let data = await networkClient.request("/user")
// <- suspend point:线程 A 被释放
// resume:可能在线程 B 上继续执行
// data 变量和函数上下文从 Continuation 恢复
let user = try JSONDecoder().decode(User.self, from: data)
return user
}
// 同时启动多个任务
func loadDashboard() async {
// 三个任务并发,但只使用 CPU 核心数个线程
async let user = fetchUserData()
async let feed = fetchFeed()
async let config = fetchConfig()
// 三个任务都在等待网络,0 个线程被阻塞
let (userData, feedData, configData) = try await (user, feed, config)
}
Cooperative Cancellation 的处理
func processItems(_ items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
// 检查是否被取消
if Task.isCancelled {
// 清理并抛出取消错误
throw CancellationError()
}
// 另一种方式:在耗时操作前检查
try Task.checkCancellation()
let result = try await processItem(item)
results.append(result)
}
return results
}
// 外部取消任务
let task = Task {
try await processItems(bigArrayOfItems)
}
// 5 秒后取消
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
task.cancel()
}
Actor 的 Serial Executor 行为
actor DataProcessor {
var processedCount = 0
// 每个方法调用都被 serial executor 排队
func process(_ item: Item) -> Result {
processedCount += 1 // 安全:同一时刻只有一个方法在执行
return item.process()
}
func getCount() -> Int {
processedCount
}
}
// 调用者视角
let processor = DataProcessor()
// 这两个调用可能并行发起
async let result1 = processor.process(item1) // 如果 processor 忙,这里会等待
async let result2 = processor.process(item2) // 排在 result1 后面
// 但 processor 内部是串行执行的
let (r1, r2) = try await (result1, result2)
最佳实践
性能调优: 不要在 async 函数中做长时间同步计算——这会阻塞协作线程池中的一个线程,影响全局调度。把 CPU 密集型工作放到 Task.detached 中(独立的执行上下文),或者用 withCheckedContinuation 桥接到 GCD 的全局队列。
取消处理: 在长时间运行的 async 循环中定期检查 Task.isCancelled。在每次循环迭代的开头调用 try Task.checkCancellation() 是最简单的模式。
还有什么值得关注
@MainActor是一个全局 Actor,它的 serial executor 就是主线程的 run loop。- Task 的优先级(priority)会影响线程池的调度顺序,但不会保证实时性。
nonisolated关键字可以让 Actor 的某个方法脱离 serial executor 执行。