Swift并发编程:async/await实战——一个北漂程序员的踩坑实录

Docker搬运工
2025-12-17 11:03
阅读 248

上个月刚在成都付完房子首付,银行卡余额比我的代码注释还干净。为了多接点外包单子、多赚点奶粉钱(虽然还没娃,但得提前准备不是?),我开始认真研究 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 并发”之前,最好搞清楚这几个问题:

  1. async/await 和 GCD 的本质区别是什么?
  2. 如何处理任务取消和超时?
  3. Actor 模型如何解决数据竞争?
  4. 在 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝