Swift async/await 全面详解
async/await 是 Swift 5.5 引入的结构化并发核心语法,彻底重构了 Swift 的异步编程范式,让异步代码可以像同步代码一样线性书写,消除了传统回调嵌套(回调地狱),同时提供编译期安全检查、统一错误处理和自动化的任务生命周期管理,是当前 Apple 平台推荐的异步编程标准方案Apple Developer。
一、核心基础语法
1. async:标记异步函数
async 关键字用于标记一个函数 / 方法 / 计算属性为异步函数,声明该函数具备「挂起 - 恢复」的能力,执行过程中可以主动让出线程,等待异步操作完成后再恢复执行。
语法规则
- 位置:函数参数括号之后,
throws之前(如有),返回值之前 - 异步函数只能在异步上下文中被调用
- 支持与
throws结合,实现异步错误抛出
Swift
// 基础异步函数
func fetchData(from url: URL) async -> Data {
// 异步操作实现
}
// 可抛出错误的异步函数(最常用)
func fetchUser() async throws -> User {
let (data, _) = try await URLSession.shared.data(from: userURL)
return try JSONDecoder().decode(User.self, from: data)
}
2. await:标记挂起点
await 关键字用于标记异步函数的调用点,这里是潜在的挂起点:执行到此处时,当前任务会被挂起,让出线程资源给其他任务,直到异步函数执行完成,再恢复后续代码的执行。
核心特点
- 挂起≠阻塞:挂起时线程不会被锁死,会被 Swift 运行时回收,调度执行其他任务,完全避免了线程阻塞的性能损耗
- 仅标记潜在挂起:如果异步操作已完成,会直接同步执行后续代码,不会真的挂起
- 必须与
async配对:只能在async标记的异步上下文中使用
调用示例
Swift
// 异步函数中调用
func loadUserInfo() async {
do {
// 调用异步函数,标记挂起点,等待结果返回后再往下执行
let user = try await fetchUser()
print("用户信息:\(user)")
// 后续业务逻辑
} catch {
print("请求失败:\(error)")
}
}
// 同步上下文调用:必须用Task创建异步执行环境
// 比如UIKit的按钮点击事件(同步函数)中调用异步代码
@IBAction func loadButtonTapped(_ sender: UIButton) {
Task {
await loadUserInfo()
}
}
二、核心优势:对比传统回调式编程
传统异步编程依赖 GCD 的 completion handler 回调,极易出现多层嵌套的「回调地狱」,代码可读性差、错误处理分散、容易遗漏回调调用,而 async/await 彻底解决了这些问题。
对比示例:网络请求嵌套
1. 传统 GCD 回调写法
Swift
// 回调式API
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: userURL) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(FetchError.noData))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(error))
}
}.resume()
}
// 调用:多层嵌套回调地狱
fetchUser { result in
switch result {
case .success(let user):
// 嵌套请求用户头像
fetchAvatar(userId: user.id) { avatarResult in
switch avatarResult {
case .success(let avatar):
// 嵌套切回主线程更新UI
DispatchQueue.main.async {
self.avatarImageView.image = avatar
}
case .failure(let error):
print("头像加载失败:\(error)")
}
}
case .failure(let error):
print("用户加载失败:\(error)")
}
}
2. async/await 写法
Swift
// async/await 风格API
func fetchUser() async throws -> User {
let (data, _) = try await URLSession.shared.data(from: userURL)
return try JSONDecoder().decode(User.self, from: data)
}
func fetchAvatar(userId: String) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: avatarURL(userId: userId))
guard let image = UIImage(data: data) else {
throw FetchError.invalidImage
}
return image
}
// 调用:线性书写,无嵌套,逻辑清晰
func loadUserAvatar() async {
do {
// 顺序执行,先获取用户,再获取头像
let user = try await fetchUser()
let avatar = try await fetchAvatar(userId: user.id)
// 主线程更新UI
await MainActor.run {
self.avatarImageView.image = avatar
}
} catch {
// 统一错误处理
print("加载失败:\(error)")
}
}
核心优势总结
- 代码线性书写:彻底消除嵌套回调,逻辑顺序与代码执行顺序完全一致,可读性和可维护性大幅提升
- 统一错误处理 :与同步代码完全一致的
try/do-catch机制,无需在每个回调中单独处理错误,避免遗漏 - 编译期安全保障:编译器强制要求处理错误,杜绝了回调式 API 中「忘记调用 completion」的致命 bug
- 自动化生命周期管理:基于结构化并发,任务有明确的层级和作用域,父任务会自动等待子任务完成,避免内存泄漏和任务失控
- 极低的调度开销:基于协程实现,挂起 / 恢复仅在用户态完成,无需内核态的线程切换,性能远超传统线程切换
三、底层执行原理
1. 协程与状态机
Swift 的 async/await 基于协程(Coroutine) 实现,编译器会将异步函数转换为一个状态机,管理函数的挂起与恢复:
- 每个
await挂起点对应一个状态,编译器会自动保存当前函数的执行上下文(局部变量、程序计数器、栈帧等) - 挂起时,上下文被保存到堆中,线程被释放回全局协作线程池,执行其他任务
- 异步操作完成后,系统恢复保存的上下文,从挂起的状态继续执行后续代码
2. 协作式线程池
Swift 并发系统内置了一个全局协作线程池,线程数量与设备 CPU 核心数匹配,彻底避免了 GCD 中常见的「线程爆炸」问题:
- 线程池仅负责执行就绪的异步任务,任务挂起时立即回收线程,最大化 CPU 资源利用率
- 开发者无需手动管理线程、队列,所有调度都由 Swift 运行时自动完成
- 保证同一时间不会有超过 CPU 核心数的线程处于运行状态,减少内核态的线程调度开销
3. 挂起 vs 阻塞的核心区别
| 行为 | 特点 | 线程状态 | 资源占用 |
|---|---|---|---|
| await 挂起 | 协作式让出线程,保存上下文,可恢复 | 线程被释放,执行其他任务 | 极低,仅保存上下文 |
| 线程阻塞 | 强制占用线程,等待事件完成 | 线程休眠,无法执行其他任务 | 高,持续占用线程资源 |
四、进阶核心用法
1. 并行执行异步任务
对于无依赖的多个异步任务,使用 async let 可以实现并行执行,总耗时等于耗时最长的单个任务,而非所有任务耗时之和,大幅提升执行效率。
Swift
// 串行执行:总耗时 = 任务1耗时 + 任务2耗时
let user = try await fetchUser()
let articles = try await fetchArticles()
// 并行执行:总耗时 = max(任务1耗时, 任务2耗时)
// async let 立即启动异步任务,不会阻塞当前执行
async let user = fetchUser()
async let articles = fetchArticles()
// 此处await等待两个任务全部完成
let (fetchedUser, fetchedArticles) = try await (user, articles)
2. 任务取消与协作式取消
Swift 结构化并发支持协作式取消:父任务取消时,会自动向所有子任务传播取消信号,子任务可以检查取消状态,提前终止执行。
核心用法
Swift
// 支持取消的异步函数
func fetchLargeFile() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: largeFileURL)
// 检查任务是否被取消,已取消则抛出CancellationError
try Task.checkCancellation()
// 也可以手动判断取消状态
if Task.isCancelled {
throw CancellationError()
}
return processLargeData(data)
}
// 手动取消任务
let downloadTask = Task {
try await fetchLargeFile()
}
// 触发取消
downloadTask.cancel()
3. 适配传统回调式 API
对于老的基于 completion handler 的 API,可以通过 withCheckedThrowingContinuation(支持错误)/ withCheckedContinuation(无错误) 快速封装为 async/await 风格的 API。
封装规则
- 续体(continuation)必须且只能 resume 一次,多次调用会触发运行时错误
- 无论成功 / 失败,都必须调用 resume,否则会导致任务永久挂起
Swift
// 传统回调API
func legacyFetchUser(userId: String, completion: @escaping (Result<User, Error>) -> Void) {
// 原有实现
}
// 封装为async/await API
func fetchUser(userId: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
legacyFetchUser(userId: userId) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
4. 主线程调度与 MainActor
UI 相关操作必须在主线程执行,Swift 并发模型通过 @MainActor 提供编译期的主线程保障,替代传统的 DispatchQueue.main.async。
核心用法
Swift
// 标记整个类/结构体的所有方法、属性都在主线程执行
@MainActor
class UserProfileViewModel: ObservableObject {
@Published var avatar: UIImage?
func loadAvatar() async throws {
let user = try await fetchUser()
let avatar = try await fetchAvatar(userId: user.id)
// 无需手动切主线程,@MainActor保证此处在主线程执行
self.avatar = avatar
}
}
// 标记单个函数在主线程执行
@MainActor
func updateAvatarUI(_ image: UIImage) {
avatarImageView.image = image
}
// 异步函数中调用主线程方法
func loadUserAvatar() async {
do {
let user = try await fetchUser()
let avatar = try await fetchAvatar(userId: user.id)
// 等待主线程执行完成
await updateAvatarUI(avatar)
} catch {
print(error)
}
}
五、系统版本与兼容性
- 原生支持:Swift 5.5+,iOS 15+/macOS 12+/watchOS 8+/tvOS 15+
- 回退支持:Xcode 13.2+,Swift 5.5+,可向下兼容到 iOS 13+/macOS 10.15+(部分高级特性受限)
- 系统 API 适配:Apple 官方框架(URLSession、UIKit、HealthKit 等)已全面提供 async/await 版本的 API,无需手动封装Apple Developer
六、最佳实践与注意事项
-
禁止在 async 函数中执行长时间同步阻塞操作全局协作线程池的线程数量有限,长时间阻塞线程会导致线程池无法调度其他任务,引发性能问题甚至死锁。耗时的同步操作应单独封装,避免占用并发线程池。
-
优先使用结构化并发,避免滥用 Task.detached
Task.detached会创建脱离父任务层级的分离任务,无法自动传播取消、无法自动等待完成,极易导致任务失控和内存泄漏,优先使用Task、async let、TaskGroup等结构化并发 API。 -
UI 操作必须使用 MainActor 保障主线程执行 所有视图更新、UI 事件处理都必须在主线程执行,优先使用
@MainActor标记相关类型和方法,避免手动切换主线程导致的错误。 -
主动响应任务取消 耗时较长的异步函数中,应在关键节点通过
Task.checkCancellation()检查取消状态,及时释放资源、终止执行,避免无效的资源占用。 -
避免过度创建并行任务 虽然
async let和TaskGroup支持并行执行,但大量并行任务会导致系统资源耗尽,应根据业务场景控制并行任务的数量。