Swift并发编程:async/await如何让我少掉几根头发

线程池保洁员
2026-05-19 00:00
阅读 472

上周五晚上十一点,实验室的空调嗡嗡作响,我盯着VSCode里一堆红彤彤的编译错误,心里直骂娘。导师临时塞给我一个iOS端AI图像预处理模块的活儿,要求下周三前集成进主App——没错,就是那种“需求很简单,就改一点点”的经典场景。更坑的是,这个模块要调用本地Core ML模型做推理,还得从服务器拉配置文件,网络请求和计算密集型任务混在一起,老式的回调地狱写法已经让我在review代码时血压飙升。

作为一枚在上海租房、每天通勤不到15分钟的211软工研二狗,我深知在这种deadline压力下,代码不仅要跑得起来,更要安全、清晰、可维护。于是,我果断祭出了Swift 5.5引入的async/await——这套并发模型,简直是我这种既要赶工又要保命的研究生的救命稻草。

被逼上梁山:为什么非得用async/await?

其实一开始我是拒绝的。毕竟我们组主力项目还是Objective-C混Swift的老架构,很多前辈写的网络层还是基于URLSession回调 + delegate那一套。但这次不一样:AI提效是今年实验室的重点方向,领导明确说“新模块必须用现代Swift写法”,还特意提到“别整那些嵌套十层的completion handler了,看着头晕”。

而且说实话,回调写法在复杂流程里真的容易出安全问题。比如:

  • 忘记在主线程更新UI,导致crash
  • 多个异步操作竞态条件,数据错乱
  • 错误处理分散,try-catch覆盖不全
  • 内存泄漏(闭包强引用self没解)

有一次我手滑把[weak self]写成[unowned self],结果App在低内存设备上直接闪退,被测试小姐姐追着问了三天。那一刻我就发誓:再不用回调了!

async/await初体验:告别回调地狱

先看个最简单的例子。以前拉个用户信息,你可能这么写:

func fetchUserProfile(completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: userURL) { data, response, error in
        DispatchQueue.main.async {
            if let error = error {
                completion(.failure(error))
                return
            }
            // 解析data...
            completion(.success(user))
        }
    }.resume()
}

调用的时候更是灾难:

fetchUserProfile { [weak self] result in
    switch result {
    case .success(let user):
        self?.fetchUserAvatar(userId: user.id) { avatarResult in
            // 又一层...
        }
    case .failure(let error):
        // 错误处理
    }
}

现在用async/await,清爽多了:

func fetchUserProfile() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    return try JSONDecoder().decode(User.self, from: data)
}

// 调用
Task {
    do {
        let user = try await fetchUserProfile()
        let avatar = try await fetchUserAvatar(userId: user.id)
        // 直接顺序写!
        await MainActor.run {
            self.updateUI(with: user, avatar: avatar)
        }
    } catch {
        handleError(error)
    }
}

注意几个关键点:

  1. async标记函数是异步的
  2. await用于等待异步操作完成
  3. 错误统一用throws/try/catch处理
  4. UI更新显式用MainActor.run,避免线程错误

这种写法不仅逻辑线性清晰,更重要的是编译器能帮我们做很多安全检查。比如你忘了await,Xcode会直接报错;你在一个非async函数里调用async函数,也会提示你包装在Task里。

实战踩坑:AI图像处理模块的并发设计

回到我的AI图像预处理需求。整个流程大概是:

  1. 从服务器获取最新模型配置(JSON)
  2. 根据配置决定是否下载新Core ML模型
  3. 加载模型到内存
  4. 对传入图片做推理
  5. 返回结果并缓存

用传统方式,这至少要嵌套三层回调。但现在,我可以这样组织:

@MainActor
class ImageProcessor {
    private var currentModel: MLModel?
    
    func processImage(_ image: UIImage) async throws -> ProcessedResult {
        // 步骤1:获取配置
        let config = try await fetchModelConfig()
        
        // 步骤2:按需更新模型
        if needsUpdate(currentModel, with: config) {
            let modelURL = try await downloadModel(config.modelURL)
            currentModel = try await loadModel(from: modelURL)
        }
        
        // 步骤3:执行推理
        guard let model = currentModel else { throw ProcessorError.noModel }
        return try await model.predict(image: image)
    }
}

看起来是不是像同步代码?但每一步都是异步的!这就是结构化并发的魅力。

坑点一:actor隔离与状态管理

刚开始我把currentModel直接声明为普通属性,结果在并发测试时偶尔crash。查了半天才发现:多个Task同时调用processImage时,可能同时触发模型更新,导致数据竞争。

解决方案:用actor隔离状态。

actor ModelManager {
    private var currentModel: MLModel?
    
    func updateIfNeeded(with config: ModelConfig) async throws -> MLModel {
        if needsUpdate(currentModel, with: config) {
            let modelURL = try await downloadModel(config.modelURL)
            currentModel = try await loadModel(from: modelURL)
        }
        return currentModel!
    }
}

然后在processor里持有ModelManager实例。这样所有对模型的操作都被串行化,天然线程安全。

坑点二:取消操作的支持

产品经理总喜欢加需求:“用户切页面时要能取消正在处理的图片”。以前用URLSession还能靠task.cancel(),现在怎么办?

Swift的Task天生支持取消!关键是在耗时操作中定期检查Task.isCancelled

func downloadModel(_ url: URL) async throws -> URL {
    let (data, _) = try await URLSession.shared.data(from: url)
    
    // 模拟大文件处理,分块检查取消
    for chunk in data.chunks(ofCount: 1024*1024) {
        if Task.isCancelled {
            throw CancellationError()
        }
        // 处理chunk...
    }
    return saveToDisk(data)
}

调用方只需保留Task引用,需要取消时调task.cancel()即可。比以前手动管理各种cancel token简单多了。

坑点三:与老代码的兼容

我们主App还有大量Objective-C代码,怎么让OC调用新的async方法?

Apple提供了async桥接方案,但需要额外工作:

  1. 在Swift类里提供同步包装方法
  2. Task启动异步操作,通过回调返回结果
// Swift端
@objc func processImageSync(_ image: UIImage, completion: @escaping (NSData?, NSError?) -> Void) {
    Task {
        do {
            let result = try await processImage(image)
            completion(result.data, nil)
        } catch {
            completion(nil, error as NSError)
        }
    }
}

// OC调用
[[ImageProcessor shared] processImageSync:image 
                              completion:^(NSData *result, NSError *error) {
    // 处理结果
}];

虽然有点啰嗦,但至少保证了平滑过渡。等哪天整个项目Swift化了,再把这些胶水代码删掉。

性能与安全:比GCD更靠谱的选择

我知道有人会说:“GCD不是也能做并发吗?何必学新东西?” 但实际用下来,async/await在安全性和可读性上优势太明显了。

特性 GCD async/await
错误处理 分散在各处 统一try/catch
线程安全 手动管理队列 actor自动隔离
取消支持 需手动实现 内置Cancellation
调用栈 回调打断 连续堆栈(方便调试)
编译检查 强(忘await会报错)

特别是最后一点——编译器能揪出潜在的并发bug,这对赶deadline的我们来说简直是神技。上周我同事用GCD写了个缓存模块,结果因为没正确使用dispatch_barrier_async导致数据错乱,线上回滚了两次。而我的async版本,光靠编译器检查就避开了类似问题。

AI提效?不,是代码人生提效

说到AI提效,最近实验室确实在推通义千问辅助编程。但我发现,与其依赖AI生成复杂并发代码(它经常搞错线程上下文),不如先掌握好Swift原生的并发模型。async/await本身就是一种“提效”——它让异步代码像同步一样好写,减少心智负担。

记得去年双11期间,我还在用回调写支付流程,结果因为漏处理一个error case,导致订单状态卡住,被运维大哥半夜call醒。现在有了structured concurrency,这种低级错误基本绝迹。代码人生本该如此:写得安心,睡得踏实。

上架App Store的小贴士

顺便分享个经验:用async/await的App上架完全没问题,只要你的Deployment Target ≥ iOS 15。不过要注意:

  • 如果支持iOS 13/14,得用#available做版本判断
  • 不要把async函数暴露给OC头文件(会编译失败)
  • 审核时确保后台任务有合理超时(别让Task无限期挂着)

我们上周提审的新版本,审核只用了8小时——Apple似乎很喜欢看到开发者用新API写安全代码。

写在最后

从被导师push着学async/await,到如今把它当作日常开发标配,我深刻体会到:好的并发模型不是炫技,而是让程序员少犯错的基础设施。在这个AI满天飞的时代,工具再智能,也替代不了我们对代码安全性的敬畏。

下次当你面对复杂的异步流程时,别犹豫,试试async/await吧。说不定,它能帮你保住最后几根头发——要知道,在上海租房的压力下,发量可是硬通货啊!

(完)


作者:某211高校软件工程研二在读,现于上海某AI实验室搬砖。日常用VSCode+一堆插件写Swift/Rust,梦想是写出零crash的代码。最近沉迷Rust的所有权模型,觉得和Swift的actor有异曲同工之妙……

评论 0

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