返回文章列表

Swift Concurrency 系列 01|为什么 Swift 要引入 async/await?

它不是语法糖,而是 Swift 试图把并发从“靠约定”升级成“语言能力”

很多人第一次看到 async/await,都会把它理解成“回调写法的升级版”。

这种理解只对了一半。

如果只是想把代码写得更短、更像同步风格,那别的语言也早就做过类似事情了。Swift 真正想解决的,并不是“闭包太丑”,而是旧的异步模型已经越来越难支撑现代 iOS 项目的复杂度。

过去你可能只需要处理一个按钮点下去发一个请求,现在一个普通页面都可能同时涉及:

  • 首屏并行加载多个资源
  • 页面离开时取消旧任务
  • 本地缓存和远端请求竞争同一份状态
  • 登录态失效后自动刷新 token
  • 主线程 UI 更新和后台处理穿插发生

一旦业务到了这个复杂度,异步问题就不再只是写法问题,而是系统设计问题。async/await 的意义,恰恰在于它把这类问题从“库级别习惯”推进到了“语言级别规则”。

一、旧模型真正的问题,不是回调地狱,而是控制流被打碎

大家很喜欢用“回调地狱”来说明旧异步写法的问题,但如果只停在“缩进太深”,其实还是没抓到重点。

回调真正麻烦的地方在于:业务本来是一条线,但代码被拆成了很多离散片段。

比如一个非常普通的初始化流程:

  1. 拉当前用户信息
  2. 根据用户权限决定首页模块
  3. 再拉首页数据
  4. 如果失败,记录埋点
  5. 最后回到主线程更新 UI

这在脑子里是一条顺序非常清楚的链路。但在 completion 时代,这条链路经常会被拆成:

  • 一个请求闭包
  • 一个权限判断闭包
  • 若干层错误处理
  • 一个主线程切换
  • 若干早返回分支

等代码一长,你很难一眼说清:

  • 主流程是什么
  • 哪些分支会中断
  • 哪一步失败后该由谁兜底
  • 页面离开后还该不该继续

异步逻辑真正难维护的地方,不是能不能实现,而是意图和表达方式脱节了

二、completion 最大的问题不是“老”,而是它把太多关键语义交给约定

completion 不是坏东西。今天很多底层 API 依然很好用,而且在某些需要多次回调、流式输出、桥接旧系统的场景里仍然很合适。

问题在于,当一个系统大量依赖 completion 时,很多重要语义都只是“团队习惯”,而不是语言规则。

比如下面这些事情,在 completion 模型里往往靠大家默认理解:

  • 这个回调到底会调用一次还是多次
  • 成功和失败是不是严格互斥
  • 回调会在哪个线程回来
  • 调用者是否有取消能力
  • 中途如果对象释放,结果还该不该继续送达

你会发现,这些都不是小问题,而是并发系统的核心问题。

一旦这些语义靠文档、靠命名、靠经验来维持,项目越大,语义就越容易发散。最后最痛苦的通常不是“某个 API 不会用”,而是你无法稳定推理一段异步代码到底会怎么跑

三、Swift 引入 async/await,本质上是在收紧异步代码的规则

async/await 的价值,不只是把:

fetchUser { result in
    ...
}

换成:

let user = try await fetchUser()

更重要的是,它把很多原来散落在约定里的东西,重新拉回到了语言层。

比如现在:

  • 一个 async 函数有清晰的返回点
  • 失败通过 throw 传播,而不是靠不同风格的结果回调
  • await 显式告诉你,这里是暂停点
  • 异步调用链可以顺着读下去,不需要在多个闭包之间跳来跳去

这件事的意义不在“好看”,而在“规则更紧了”。

复杂系统真正怕的从来不是代码多,而是规则松。
一旦规则松,代码就会越来越依赖“这个人刚好懂上下文”。

四、它提升的不只是可读性,而是可推理性

很多人会说:completion 我也能看懂,为什么非得学 async/await

问题不在于你今天能不能看懂,而在于:

  • 三个月后你还能不能快速看懂
  • 同事接手时能不能靠代码本身推理出流程
  • 出 bug 时你能不能从入口顺着走到出口

这就是“可读性”和“可推理性”的差别。

可读性是“这段代码我今天看得顺不顺眼”。
可推理性是“我能不能基于这段代码判断:失败怎么传、取消怎么生效、状态在哪一层被改掉”。

比如下面这几个问题,在旧模型里常常非常难追:

  • 用户离开页面后,这条请求链还在不在
  • 第三步失败时,最终页面状态会停在哪
  • 两个异步结果同时回来时,谁有资格写 UI

async/await 当然不会自动替你回答这些问题,但它至少让这些问题被放在更清楚的流程结构里,而不是隐藏在碎片化的闭包里。

五、为什么这件事在 iOS 项目里越来越关键

iOS 项目今天最大的变化之一,是“异步已经从边缘能力变成主干能力”。

以前异步更多是“点一下,发个请求”。现在异步直接参与页面生命周期、状态机、缓存层、权限系统、埋点系统,甚至参与整个产品响应速度的设计。

一个真实业务页面常见的情况是:

  • 页面一打开就有首屏请求
  • 某些模块走本地缓存兜底
  • 某些请求依赖用户资料或实验配置
  • 某些操作需要防重入
  • 某些结果必须回主线程才能安全改状态

在这种前提下,如果异步系统还只是“大家各写各的 completion”,代码很快就会从“能工作”滑向“没人敢改”。

所以 async/await 真正接住的,是现代 App 中异步已经成为默认工作方式这件事。

六、它不是终点,而是 Swift 整个并发模型的入口

如果只看这一条语法特性,async/await 好像只是更好的异步表达。

但如果放到 Swift Concurrency 整体里看,它其实是在为后面的能力铺路:

  • Task:明确任务边界和生命周期
  • MainActor:约束 UI 相关状态的执行语义
  • Actor:隔离共享可变状态
  • 结构化并发:把子任务纳入父任务生命周期

换句话说,Swift 不只是想提供一个更顺手的异步 API,它是在试图把“并发”从一个零散技巧,变成一套可组合、可推理、可被语言检查的模型。

这就是为什么你不能把 async/await 只当成“写起来更顺手”。它背后真正连接的是一整套新的并发设计哲学。

七、它没有自动解决所有问题,但改变了问题的性质

这里也必须说清楚:有了 async/await,并发 bug 并不会自动消失。

它解决不了:

  • 你把状态边界设计错了
  • 旧任务覆盖新结果
  • 某个 ViewModel 同时承担了过多职责
  • 任务取消策略根本没想清楚

但它改变了一件非常重要的事:
很多问题不再是“因为表达过于混乱,连问题长什么样都看不清”,而是“问题已经能被清楚表达出来”。

这听起来像退一步,实际上是进一大步。因为只有当问题变得可表达,调试、review、重构才有抓手。

八、结论:Swift 引入 async/await,不是为了让你少写闭包

如果只用一句话总结这件事,我会说:

Swift 引入 async/await,不是为了让异步代码更像同步代码,而是为了让异步代码重新变得可理解、可推理、可维护。

所以它不是“好看的新语法”,而是并发模型升级的第一步。

当你理解了这一点,后面再看 TaskActorMainActor,就不会把它们当成零散语法点,而会明白 Swift 在做的是一件更大的事:
把并发从“经验技巧”重新收进“语言规则”。