Swift 对于结构化的编写异步和并行代码有着原生的支持。异步代码可以被挂起并在之后继续执行,同一时间只能有一段代码被执行。代码支持挂起和继续执行,就可以在执行耗时很长的任务时抽空执行一些快速的操作,比如在下载文件、解析文件的过程中更新 UI。并行代码指的是多段代码同时执行;比如一个拥有四核处理器的电脑可以同时四段代码,其中的每一个核都可以执行一个任务。一个使用并行和异步代码的程序可以同时执行多个运算;它可以在某个运算等待外部系统的时候挂起这个运算,从而让编写内存安全的代码更加容易。
并发和异步代码在带来时序灵活性的同时不免会增加复杂度。一些异步代码会自动包含编译时检查——比如,你可以使用 actor 来安全的访问可变的状态。然而,给一段运行缓慢并且有错误的代码添加并发能力并不能让它更快或者更正确的运行。事实上,给代码增加并发能力还有可能导致代码问题更难排查。但如果在需要并发的代码中使用 Swift 原生支持的并发能力会让你在编译阶段就发现问题。
本章剩余的部分将使用并发指代异步和并行。
注意:
如果你曾经写过并发的代码的话,那可能使用过线程。Swift 中的并发模型是基于线程的,但你不会直接和线程打交道。在 Swift 中,一个异步函数可以交出它在某个线程上的运行权,这样另一个异步函数在这个函数被阻塞时就能获得此线程的运行权。
你当然也可以不用 Swift 原生支持去写并发的代码,只不过代码的可读性会下降。比如,下面的这段代码会拉取一系列图片名称的列表,下载列表中的图片然后展示给用户:
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
在这个简单的案例中,由于代码中有一系列的 completion handler,最终你必须得使用嵌套闭包。更加复杂的代码会产生更深的嵌套,从而使代码迅速变得臃肿起来。
一、定义和调用异步函数
异步函数或异步方法是一种能在运行中被挂起的特殊函数或方法。对于普通的同步函数或方法来说,它们只能运行到完成闭包、抛出错误或者永远不返回。异步函数或方法也能做到这三件事,但同时也可以在等待其他资源的时候挂起。在异步函数或者方法的函数体中,你可以标记其中的任意位置是可以被挂起的。
为了标记某个函数或者方法是异步的,你可以在它的声明中的参数列表后边加上 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: