基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

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):

    plaintext
    func 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):

    plaintext
    func 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 让异步代码更好写、更好读、更安全,并且在底层通过更高效的线程调度模型提升了性能

00:00
00:00