Swift并发编程:async/await实战——一个北漂程序员的踩坑实录
上个月刚在成都付完房子首付,银行卡余额比我的代码注释还干净。为了多接点外包单子、多赚点奶粉钱(虽然还没娃,但得提前准备不是?),我开始认真研究 iOS 最新的并发模型。毕竟现在去面试,要是简历上没写“熟悉 Swift 并发”,HR 可能连初筛都过不了——哪怕你十年前就用 JavaScript 写过 Promise.all。
我是那种喜欢抠细节的人。以前看开源项目源码,看到一坨嵌套回调就手痒,恨不得立刻重构成链式调用。现在公司项目里一堆 completion handler 像意大利面条一样缠在一起,上周五晚上改个 Bug 调到凌晨两点,差点把 MacBook Pro 当飞盘扔出阳台。产品经理还在群里@我说:“这个需求很简单,明天上线前能搞定吧?”——我默默点了根电子烟(其实是保温杯里的枸杞水),决定彻底拥抱 async/await。
为什么是现在?
其实我不是 Apple 生态的原教旨主义者。早年写前端时,JavaScript 的异步地狱让我头发掉了一半。后来 React 出了 Hooks,Vue 有了 Composition API,异步逻辑终于能写得像同步代码一样清爽。而 Swift 在 2021 年 WWDC 上推出的 async/await,简直就是 iOS 开发者的福音。
我们团队最近在重构一个电商 App 的商品详情页。双 11 期间线上崩了好几次,排查发现是多个网络请求并行执行时线程竞争导致的数据错乱。老代码是这样写的:
func loadProductDetail() {
fetchProduct { [weak self] product in
DispatchQueue.main.async {
self?.product = product
self?.fetchRecommendations { recommendations in
DispatchQueue.main.async {
self?.recommendations = recommendations
self?.fetchReviews { reviews in
// ... 还有三个嵌套!
}
}
}
}
}
}
这代码看得我血压飙升。不仅可读性差,维护起来更是噩梦。测试同学每次提 Bug 都说“数据偶尔加载不全”,我 debug 三天才发现是某个请求超时后没正确处理 completion 回调。这种代码放 GitHub 上,怕是要被喷成筛子。
从 JavaScript 到 Swift:异步思维的迁移
说实话,刚接触 async/await 时我还真有点不适应。毕竟在 JS 里 async function 返回的是 Promise,而在 Swift 里,它返回的是一个真正的值(或者说,编译器帮你处理了 Future)。不过好在我之前啃过《Swift 并发编程实战》这本书(强烈推荐!作者把底层 Actor 模型讲得明明白白),加上常年研究开源项目的习惯,很快就摸清了门道。
先看改造后的核心逻辑:
// MARK: - 使用 async/await 重构
func loadProductDetail() async throws {
let (product, recommendations, reviews) = try await (
fetchProductAsync(),
fetchRecommendationsAsync(),
fetchReviewsAsync()
)
await MainActor.run {
self.product = product
self.recommendations = recommendations
self.reviews = reviews
}
}
// 网络请求方法标记为 async
func fetchProductAsync() async throws -> Product {
return try await URLSession.shared.data(from: productURL).product
}
是不是瞬间清爽了?三个请求并行执行,错误统一处理,UI 更新也明确指定在主线程。最关键的是——代码顺序和执行顺序一致,再也不用在回调地狱里迷失自我。
踩坑实录:那些文档没告诉你的事
当然,理想很丰满,现实很骨感。实际开发中我还是踩了不少坑,这里分享几个血泪教训。
坑 1:MainActor 和 UI 更新
一开始我以为只要在 async 函数里更新 UI 就行,结果 Xcode 直接给我报了个运行时警告:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
原来即使你用了 async/await,如果不在 MainActor 上操作,依然会触发线程安全问题。后来我养成了习惯:所有涉及 UI 或 @Published 属性的赋值,都包在 await MainActor.run { } 里。
坑 2:取消任务的正确姿势
产品经理突然改需求说“用户切换 Tab 时要取消正在加载的请求”。以前用 URLSessionDataTask 可以直接 cancel(),但现在用 async/await 怎么办?
查了文档才发现,Swift 并发模型内置了任务取消机制。关键是要在耗时操作中定期检查 Task.isCancelled:
func fetchHugeData() async throws -> Data {
let task = Task {
var data = Data()
for chunk in dataSource {
// 定期检查是否被取消
try Task.checkCancellation()
data.append(chunk)
}
return data
}
return try await task.value
}
配合 SwiftUI 的 .task { } 修饰符,页面消失时任务会自动取消,内存泄漏问题迎刃而解。
坑 3:与旧代码的兼容
公司老项目里还有大量基于 Delegate 和 NotificationCenter 的异步逻辑。总不能一次性全重写吧?还好 Swift 提供了 withCheckedContinuation 来桥接传统回调:
func migrateOldAPI() async -> Result<String, Error> {
return await withCheckedContinuation { continuation in
OldNetworkManager.shared.request { result in
continuation.resume(returning: result)
}
}
}
这样就能逐步迁移,不用跟技术债硬刚。
性能优化:不只是语法糖
很多人以为 async/await 只是让代码好看点,其实它对性能提升也有实实在在的帮助。
我们用 Instruments 测了重构前后的 CPU 占用和内存峰值:
| 指标 | 重构前 | 重构后 | 优化幅度 |
|---|---|---|---|
| 内存峰值 | 186 MB | 142 MB | ↓23.7% |
| 主线程阻塞时间 | 320 ms | 89 ms | ↓72.2% |
| 请求完成时间(并行) | 2.1s | 1.3s | ↓38.1% |
为什么性能提升这么明显?因为 async/await 底层基于协程,避免了传统 GCD 中频繁的线程切换开销。而且 Swift 编译器会对并发代码做深度优化,比如自动合并连续的异步操作。
另外,结合 TaskGroup 可以轻松实现动态并发控制:
func fetchAllProducts() async throws -> [Product] {
var products: [Product] = []
try await withThrowingTaskGroup(of: Product.self) { group in
for id in productIDs {
group.addTask {
return try await fetchProduct(id: id)
}
}
for try await product in group {
products.append(product)
}
}
return products
}
这比手动管理 DispatchGroup 简洁多了,还不容易漏掉错误处理。
对比其他方案:为什么选 async/await?
我知道有些团队还在用 RxSwift 或 Combine。作为一个喜欢研究开源项目的人,我也对比过这些方案:
| 方案 | 学习成本 | 可读性 | 调试难度 | Apple 官方支持 |
|---|---|---|---|---|
| Completion Handler | 低 | 差 | 高 | 原生 |
| RxSwift | 高 | 中 | 高 | 第三方 |
| Combine | 中 | 中 | 中 | 原生(iOS 13+) |
| async/await | 低 | 高 | 低 | 原生(iOS 15+) |
对于我们这种小团队(总共就 5 个 iOS 开发),降低新人上手成本太重要了。上周实习生第一天入职,看 async/await 代码就能直接上手改 Bug,要是换成 RxSwift 的 Observable 链,估计得先买本《响应式编程入门》压枕头底下睡一周。
给跳槽同学的建议
如果你正在准备面试,简历上写“精通 Swift 并发”之前,最好搞清楚这几个问题:
- async/await 和 GCD 的本质区别是什么?
- 如何处理任务取消和超时?
- Actor 模型如何解决数据竞争?
- 在 SwiftUI 中如何安全地使用 async/await?
我之前面一家大厂,面试官就问:“如果两个 async 函数同时修改同一个全局变量,会发生什么?”——这其实在考察你对 Actor 隔离的理解。正确的做法是把共享状态封装在 Actor 里:
actor ProductCache {
private var products: [String: Product] = [:]
func save(_ product: Product) {
products[product.id] = product
}
func get(id: String) -> Product? {
return products[id]
}
}
这样编译器会自动保证同一时间只有一个任务能访问内部状态,从根本上避免数据竞争。
最后一点开发心得
作为过来人,我想说:技术选型不要盲目追新,但也不能死守旧范式。async/await 不是银弹,但在合适的场景下,它确实能让代码更健壮、更易维护。
自从重构完商品详情页,线上崩溃率降了 60%,测试同学终于不用每天追着我问“为什么数据又乱了”。上周团建吃饭,产品经理居然主动给我夹菜(虽然可能是想让我接新需求...)。
最重要的是,我现在晚上能准时下班回家——要知道在成都,六点下班还能赶上火锅店的非高峰折扣。房贷压力虽然大,但至少写代码的时候心情舒畅,bug 少了,头发也保住了。
如果你也在被回调地狱折磨,不妨试试 async/await。别等到被产品经理逼到墙角才行动,毕竟我们的目标是:用更少的代码,拿更多的工资,过更舒服的生活。
(完)
附:学习资源推荐
- 书籍:《Modern Swift Concurrency》by Marin Todorov(RayWenderlich 出品,实战案例超多)
- WWDC 视频:Explore structured concurrency(必看!)
- 开源项目:参考 Alamofire 5.5+ 的 async/await 实现
- 谨慎使用:网上很多 iOS 13 兼容的 hack 方案,稳定性堪忧,不如直接要求最低 iOS 15(我们 App 用户 92% 都在 iOS 15+)

评论 0