19.2. 定义和调用异步函数

异步函数异步方法是一种能在运行中被挂起的特殊函数或方法。对于普通的同步函数或方法来说,它们只能运行到完成闭包、抛出错误或者永远不返回。异步函数或方法也能做到这三件事,但同时也可以在等待其他资源的时候挂起。在异步函数或者方法的函数体中,你可以标记其中的任意位置是可以被挂起的。

为了标记某个函数或者方法是异步的,你可以在它的声明中的参数列表后边加上 async 关键字,和使用 throws 关键字来标记 throwing 函数是类似的。如果一个函数或方法有返回值,可以在返回箭头(->)前添加 async 关键字。 比如,下面是从图库中拉取图片名称的方法:

func listPhotos(inGallery name: String) async -> [String] {
    let result = // 省略一些异步网络请求代码
    return result
}

对于那些既是异步又是 throwing 的函数,需要把 async 写在throws 关键字前边。

调用一个异步方法时,执行会被挂起直到这个异步方法返回。你需要在调用前增加 await 关键字来标记此处为可能的悬点(Suspension point)。这就像调用 throwing 函数需要添加 throws 关键字来标记在发生错误的时候会改变程序流程一样。在一个异步方法中,执行只会在调用另一个异步方法的时候会被挂起;挂起永远都不会是隐式或者优先的,这也意味着所有的悬点都需要被标记为 await

比如,下面的这段代码可以拉取图库中所有图片的名称,然后展示第一张图片:

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

因为 listPhotos(inGallery:)downloadPhoto(named:) 都需要发起网络请求,需要花费较长的时间完成。给这两个函数在返回箭头前加上 async 可以将它们定义为异步函数,从而使得这部分代码在等待图片的时候让程序的其他部分继续运行。

为了更好理解上面这段代码的并发本质,下面列举出这段程序可能的一个执行顺序:

  1. 代码从第一行开始执行到第一个 await,调用 listPhotos(inGallery:) 函数并且挂起这段代码的执行,等待这个函数的返回。
  2. 当这段代码的执行被挂起时,程序的其他并行代码会继续执行。比如,后台有一个耗时长的任务更新其他一些图库。那段代码会执行到被 await 的标记的悬点,或者执行完成。
  3. listPhotos(inGallery:) 函数返回之后,上面这段代码会从上次的悬点开始继续执行。它会把函数的返回赋值给 photoNames 变量。
  4. 定义 sortedNamesname 的那行代码是普通的同步代码,因为并没有被 await 标记,也不会有任何可能的悬点。
  5. 接下来的 await 标记是在调用 downloadPhoto(named:) 的地方。这里会再次暂停这段代码的执行直到函数返回,从而给了其他并行代码执行的机会。
  6. downloadPhoto(named:) 返回后,它的返回值会被赋值到 photo 变量中,然后被作为参数传递给 show(_:)

代码中被 await 标记的悬点表明当前这段代码可能会暂停等待异步方法或函数的返回。这也被成为让出线程(yielding the thread),因为在幕后 Swift 会挂起你这段代码在当前线程的执行,转而让其他代码在当前线程执行。因为有被 await 标记的代码得可以被挂起,所以程序中只有特定的地方才能调用异步方法或函数:

  • 异步函数,方法或变量内部的代码
  • 静态函数 main() 中被打上 @main 标记的结构体,类或者枚举中的代码
  • 游离的子任务中的代码,之后会在 任务和任务组 的非结构化并行中说明

注意

学习并行的过程中,Task.sleep(_:) 方法非常有用。这个方法什么都没有做,只是等待不少于指定的时间(单位纳秒)后返回。下面是使用 sleep() 方法模拟网络请求实现 listPhotos(inGallery:) 的一个版本:

func listPhotos(inGallery name: String) async -> [String] {
    await Task.sleep(2 * 1_000_000_000)  // 两秒
    return ["IMG001", "IMG99", "IMG0404"]
}