Swift 5.5 引入的 async/await 解决了什么问题?与 GCD 相比有何优势?
Swift 5.5 引入的 async/await 是 Swift 语言历史上最重要的特性之一,它彻底改变了我们在 iOS 和 macOS 开发中编写并发代码的方式。
简单来说,它将异步代码的编写方式变得像同步代码一样直观,同时在底层通过结构化并发(Structured Concurrency)提高了性能和安全性。
以下是它解决的具体问题以及与 GCD(Grand Central Dispatch)的详细对比优势:
一、 async/await 解决了什么问题?
在 Swift 5.5 之前,我们主要使用 GCD 和闭包(Completion Handlers)来处理异步任务。这种方式存在以下显著痛点:
1. 回调地狱(Pyramid of Doom)
当一个异步任务依赖于另一个异步任务的结果时,我们需要嵌套闭包。如果依赖链很长,代码会向右缩进,形成“金字塔”,极难阅读和维护。
旧方式 (GCD/Closures):
plaintextfunc fetchAvatar(completion: @escaping (Result<UIImage, Error>) -> Void) { login { result in switch result { case .success(let user): fetchDetails(for: user) { result in switch result { case .success(let details): downloadImage(url: details.avatarUrl) { result in // ... 终于拿到了图片 completion(result) } case .failure(let error): completion(.failure(error)) } } case .failure(let error): completion(.failure(error)) } } }新方式 (async/await):
plaintextfunc fetchAvatar() async throws -> UIImage { let user = try await login() let details = try await fetchDetails(for: user) let image = try await downloadImage(url: details.avatarUrl) return image }解决: 代码变成了线性的,逻辑一目了然。
2. 错误处理繁琐且易错
在闭包中,你必须确保在每一个可能的退出路径(guard 语句、if else 分支、switch case)都调用 completion 回调。如果漏掉一个,函数就会“挂起”,调用方永远收不到结果。
解决: async/await 结合 try/catch 机制,编译器会强制检查是否返回了值或抛出了错误,就像普通函数一样安全。
3. 条件执行困难
如果你想在异步操作中使用循环(Loop)或者条件判断(If/Else),使用闭包实现非常复杂(通常需要递归)。
解决: 使用 async/await,你可以直接使用标准的 for 循环和 if 语句。
二、 与 GCD 相比的优势
虽然 GCD 是一个非常强大的底层库,但 async/await 在语言层面和运行时层面提供了更多优势:
1. 性能:避免“线程爆炸” (Thread Explosion)
- GCD 的问题: 当你向并发队列提交大量任务,且这些任务发生阻塞(例如等待 I/O 或锁)时,GCD 会不断创建新线程来维持 CPU 的利用率。这会导致“线程爆炸”,产生大量的线程上下文切换(Context Switching)开销,消耗大量内存,甚至导致系统卡顿。
- Async/Await 的优势: 它使用协作式线程池(Cooperative Thread Pool)。
- 当代码遇到
await时,当前任务会挂起(Suspend),但不会阻塞线程。 - 该线程会被释放去执行其他任务。
- 当
await的操作完成后,任务会恢复(Resume)。 - 核心区别: Swift 运行时创建的线程数通常与 CPU 核心数相当,不会无限制创建线程,极大降低了上下文切换的开销。
- 当代码遇到
2. 内存管理:减少 [weak self] 的滥用
- GCD 的问题: 在闭包中访问
self极易造成循环引用,我们不得不大量编写[weak self]和guard let self = self else { return }。 - Async/Await 的优势: 由于代码是线性执行的,且
await挂起期间不会阻塞线程,很多场景下不需要像闭包那样复杂的捕获语义(虽然在 Task 中仍需注意生命周期,但比层层嵌套的闭包要清晰得多)。
3. 结构化并发 (Structured Concurrency)
这是 async/await 带来的核心概念。
GCD 的问题: 启动一个全局队列的异步任务后,这个任务就“野”了。如果父任务被取消,子任务往往还在跑;或者很难等待多个并发任务同时结束(虽然有
DispatchGroup,但写起来很啰嗦)。Async/Await 的优势:
- 层级关系: 子任务自动继承父任务的优先级和上下文。
- 自动取消: 如果父任务被取消,所有子任务会自动收到取消信号。
- async let / TaskGroup: 替代
DispatchGroup,能以非常简洁的语法并行运行多个任务并收集结果。
示例:并行下载三张图片
plaintext// async/await 方式 async let image1 = download("1.jpg") async let image2 = download("2.jpg") async let image3 = download("3.jpg") let images = try await [image1, image2, image3] // 等待所有完成
4. 编译器安全检查
- GCD: 编译器无法知道你是否在闭包里正确处理了所有逻辑分支。
- Async/Await: 它是 Swift 语言的一部分。编译器会检查类型安全、是否抛出错误、是否在并发上下文中正确使用了 Actor 隔离的数据(防止数据竞争)。
总结
| 特性 | GCD (Grand Central Dispatch) | Swift Async/Await |
|---|---|---|
| 代码风格 | 嵌套闭包,回调地狱 | 线性代码,类似同步代码 |
| 错误处理 | 手动调用 completion,容易遗漏 | try/catch/throw,编译器强制检查 |
| 线程模型 | 阻塞线程,可能导致线程爆炸 | 挂起任务(Continuation),固定线程池 |
| 并发控制 | DispatchGroup, Semaphore (复杂) | async let, TaskGroup (简洁) |
| 安全性 | 运行时容易出现数据竞争 | 编译期检查 (Sendable, Actors) |
一句话总结: async/await 让异步代码更好写、更好读、更安全,并且在底层通过更高效的线程调度模型提升了性能。