探索 Swift 中的 async/await: 构建更清晰的并发模型

1,008 阅读4分钟

[原文地址](mp.weixin.qq.com/s?__biz=Mzg…

异步是 iOS 开发中非常常见的操作,之前通常会通过 completion 回调的方式来返回异步结果,为了更好的解释这一点,我们先看一个例子:

fetchImages { result in
    switch result {
    case .success(let images):
        print("Fetched (images.count) images.")
    case .failure(let error):
        print("Fetching images failed with error (error)")
    }
}

即使在这种简单的情况下,由于代码必须编写为一系列完成处理程序,最终会编写嵌套闭包。在这种风格下,具有深度嵌套的更复杂代码很快就会变得笨拙不堪。例子如下:

fetchImages { result in
    switch result {
    case .success(let images):
        print("Fetched (images.count) images.")
        
        resizeImages(images) { result in
            switch result {
            case .success(let images):
                print("Decoded (images.count) images.")
            case .failure(let error):
                print("Decoding images failed with error (error)")
            }
        }
    case .failure(let error):
        print("Fetching images failed with error (error)")
    }
}

传统异步编程的痛点:

  • 回调地狱:
    传统的异步编程方式通常使用回调函数,当一个异步操作完成时,会调用回调函数来处理结果。如果有多个异步操作需要依次执行,就会导致代码嵌套层级过深,难以理解和维护。
  • 错误处理复杂:
    在传统的异步编程中,错误处理需要在回调函数中进行,这使得错误处理逻辑分散,难以追踪。

async/await

Swift 中的 async/await 是一个用于编写异步代码的语法糖,它让异步代码看起来像同步代码一样,从而提高代码的可读性和可维护性。

通过 async/await 来改造文章开头的代码:

do {
    let images = try await fetchImages()
    let resizedImages = try await resizeImages(images)    
    print("Fetched (images.count) images.")
} catch {
    print("Fetching images failed with error (error)")
}

执行顺序是线性的,因此很容易跟踪和理解,代码的可读性增强了。当我们执行复杂的任务时,理解异步代码将会更加容易。

async/await 优势:

  • 更易读的代码:
    async/await 使得异步代码看起来更像同步代码,消除了回调函数带来的嵌套结构,使代码更易于理解和维护。
  • 更简洁的错误处理:
    async/await 允许使用 try/catch 块来处理异步操作中的错误,与同步代码的错误处理方式一致。
  • 更安全的并发:
    async/await 基于 Swift 的结构化并发模型,可以有效地避免数据竞争和死锁等并发问题。

async/await 使用

在异步函数的定义前添加 async 关键字,表示该函数是一个异步函数。如果函数需要标记抛出错误使用 throws。如果函数有返回值,需要在返回箭头(->)之前写上 async。

func fetchImages() async throws -> [UIImage] {
    // perform data request
}

在需要等待异步操作完成的地方使用 await 关键字。

do {
    let images = try await fetchImages()
    print("Fetched (images.count) images.")
} catch {
    print("Fetching images failed with error (error)")
}

异步函数在执行过程中,需要等待某些事情时,它也可以在中间暂停。在异步函数或方法的主体内部,你可以标记每一个需要被暂停执行的地方。

并行执行异步函数

使用 await 调用异步函数一次只运行一段代码。在异步代码运行时,调用者会等待该代码执行完成,然后再继续执行下一行代码。例如:

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

可以使用 async-let 来实现异步代码的并行执行:

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

异步函数使用环境

首次使用 async-await 时,可能会遇到下面的错误:

17240607763095.png

这个错误是因为在不支持并发的同步调用环境中调用了一个异步方法而引起的。我们可以通过将我们的 fetchData 方法也定义为异步来解决这个错误:

func fetchData() async {
    do {
        try await fetchImages()
    } catch {
        // .. handle error
    }
}

然而,这样做会将错误转移到不同的位置。相反,我们可以使用 Task.init 方法从一个支持并发的新任务中调用异步方法,并将结果分配给我们的视图模型中的属性:

final class ContentViewModel: ObservableObject {
    
    @Published var images: [UIImage] = []
    
    func fetchData() {
        Task { @MainActor in
            do {
                self.images = try await fetchImages()
            } catch {
                // .. handle error
            }
        }
    }
}

参考资料

  1. docs.swift.org/swift-book/…
  2. www.avanderlee.com/swift/async…