Swift 并发编程:深入理解 async/await 原理与实战
本文最后更新于 2026年7月24日 早上
引言
在 Swift 5.5 中,Apple 引入了全新的并发模型,其中 async/await 无疑是最重要的特性之一。它彻底改变了我们编写异步代码的方式,让原本复杂的闭包嵌套(俗称”回调地狱”)变得像同步代码一样直观易读。
本文将深入浅出地讲解 async/await 的工作原理,并配合一个简单的网络请求 Demo,让你快速上手。
一、传统异步编程之痛
在 async/await 问世之前,我们通常使用 逃逸闭包(@escaping Closure) 或 Combine 框架 来处理异步任务。
1 | |
这种方式的缺点显而易见:
- 嵌套层次深:多个连续的异步操作会导致金字塔状的代码。
- 错误处理复杂:需要在每个闭包中处理
Error。 - 逻辑割裂:代码的执行顺序不直观。
二、async/await 的核心原理
async/await 是建立在 协程(Coroutine) 概念之上的编译器级特性。它并不是多线程的简单封装,而是一种挂起与恢复的机制。
1. 状态机(State Machine)转换
当你在函数前面加上 async 关键字,编译器会将该函数编译成一个状态机。每个 await 调用都是一个挂起点(Suspension Point)。
- 挂起(Suspend):当遇到
await时,当前线程不会被阻塞,而是将函数的执行状态(局部变量、执行位置)保存起来,并将控制权交还给系统。 - 恢复(Resume):当异步操作完成(无论是成功还是失败),系统会找到合适的线程,恢复之前保存的状态,并继续往下执行。
2. 结构化并发(Structured Concurrency)
Swift 强制要求 async 任务必须在一个 Task 中运行。Task 提供了执行上下文,并支持取消操作。这种结构保证了任务的生命周期与父任务绑定,防止资源泄漏。
3. 线程调度
async/await 本身不绑定特定线程。在挂起期间,线程可以去做其他工作;恢复时,任务可能会被调度到任意一个可用线程上(除非使用 @MainActor 强制指定主线程)。
三、实战 Demo:简易网络请求
我们用一个真实的场景:请求一个公开的 API(如 https://api.github.com/users/apple),获取用户信息并打印。
步骤 1:定义数据模型
1 | |
步骤 2:封装异步请求函数
1 | |
注意:
URLSession.shared.data(from:)是 Apple 提供的原生async方法。
步骤 3:主流程调用与打印
由于 Swift 顶层不能直接调用 async 函数,我们需要在 Task 块中执行。
1 | |
四、运行结果与代码分析
当你运行上述代码时,控制台会立即打印 开始请求...,然后等待约 1-2 秒(取决于网络),最后输出用户信息。
关键点解析:
- **
try await**:try表示该方法可能抛出错误,await表示此处可能挂起。 - 非阻塞:在
await期间,线程并没有被占用,它可以去处理其他任务(例如 UI 渲染)。 - 错误传播:
throws关键字让错误可以像同步代码一样向上冒泡,无需在闭包中层层传递。
五、进阶:关于 Task 与 MainActor
1. 什么是 Task?
Task 是一个异步操作的执行单元。它从当前上下文继承优先级和取消状态。
- Task.detached:创建一个独立的任务,不继承父任务的上下文(较少使用)。
- 默认的
Task {}会继承当前 Actor 的上下文。
2. 回到主线程更新 UI
如果我们在一个后台任务中获取数据,需要更新 UI,必须回到主线程。Swift 提供了 @MainActor 来标记:
1 | |
六、总结
| 特性 | 闭包回调 | async/await |
|---|---|---|
| 代码可读性 | 差(金字塔) | 优秀(线性) |
| 错误处理 | 复杂,需传递 | 简单,原生 try/catch |
| 线程管理 | 手动切换 | 自动挂起/恢复,调度优化 |
| 调试体验 | 难(断点难定位) | 好(类似同步函数) |
async/await 的本质是编译器将异步代码转换为一个高效的状态机,它极大地提升了 Swift 开发者的生产力。掌握它,是迈向现代 Swift 开发的关键一步。