简单来说就是,Swift协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。说起任务调度,我们很自然地想到线程。从任务载体的角度来讲,协程和线程在应用场景上的确有很大的重叠之处。
一、Swift协程
简单来说就是,Swift协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。说起任务调度,我们很自然地想到线程。从任务载体的角度来讲,协程和线程在应用场景上的确有很大的重叠之处。
协程最初也确实是被应用于操作系统的任务调度的。只不过后来抢占式的调度成为了操作系统的主流实现,因此以协程为执行任务单位的协作式的调度就很少出现在我们眼前了。我们现在提到线程,基本上指的就是操作系统的内核线程;而提到协程,绝大多数都是编程语言层面实现的任务载体 —— 我们看待一个线程,就好像一艘轮船一样,而协程似乎就是装在上面的一个集装箱。
从任务的承载上来讲,线程比协程更重;从调度执行的能力来讲,线程是由操作系统调度的,而协程则是由编程语言的运行时调度的。所以绝大多数的编程语言当中实现的协程都具备更加轻量和更加灵活的特点。对于高负载的服务端,协程的轻量型就表现地很突出;而对于复杂的业务逻辑,特别是与外部异步交互的场景,协程的灵活性就可以发挥作用。
对于 Swift 而言,主要应对的自然是简化复杂的异步逻辑。而针对类似的场景,各家实际上已经给出了近乎一致的语法:async/await。其中 async 用于修饰函数,将其声明为一个异步函数,await 则用于非阻塞地等待异步函数的结果 —— Swift 也不能免俗。
async/await
为了快速了解 Swift 协程的语法,我们先给出一段代码,让大家感受一下它的样子。
在这个例子当中,我们使用 Alamofire 这个网络框架发起网络请求:
static func getImageData(url: String) async throws -> Data{
try await AF.request(url).responseDataAsync() // 调用异步函数,挂起等待结果
}
这个 responseDataAsync 函数是我对 Alamofire 框架当中的 DataRequest 做的一个扩展:
extension DataRequest {
func responseDecodableAsync<T: Decodable>(…) async throws -> T {
…
}
}
它的具体实现我们将在后面给出。
我们先请大家观察这两个函数的形式与普通函数有什么不同。我相信你很容易就能看出来,函数声明的返回值处多了个 async,而在调用函数的时候则多了个 await。使用 async 修饰的函数与普通的同步函数不同,它被称作异步函数。异步函数可以调用其他异步函数,而同步函数则不能调用异步函数。
正如我们前面提到的,async/await 这样的形式其实也是现在主流编程语言所支持的方式,例如:
JavaScript
async function delay(seconds) {
…
}
async function asyncCall() {
await delay(2); // 调用异步函数,挂起等待结果
…
}
我们看到在 JavaScript 当中同样可以通过 async 关键字来声明一个支持挂起调用的异步函数,而在想要调用另一个异步函数的时候,则需要使用 await。从形式上来看,Swift 只是把 async 放到了函数声明的后面而已。
我们不妨也看一下 Kotlin 的的协程,Kotlin 当中也有异步函数的概念,只不过它选择了 suspend 这个关键字,因此我们在 Kotlin 当中更多的称这样的函数为挂起函数(其实是可挂起的函数):
Kotlin
suspend fun delay(seconds: Long) {
…
}
suspend fun asyncCall() {
delay(2) // 调用 suspend 函数,异步挂起
}
从语法的形式上来看,Kotlin 的 suspend 关键字在函数声明时充当了 async 的作用,把函数声明为异步函数;而在调用 suspend 函数的时候则直接相当于强加了 await,如果被调用的 suspend 函数会挂起,那么我们在这个调用点也就只能挂起当前异步函数来等待被调用的异步函数的结果返回了。实际上 Swift 的异步函数调用时也会要求使用 await,而 JavaScript 的 await 则在使用和不使用时分别有不同的含义,有关这个设计问题的讨论,我们后面再探讨。
所以讲到这里我希望大家能够了解两个点:
- 这些编程语言通过 async 关键字将函数分为两类,过去的普通函数为同步函数,被修饰的函数则为异步函数。
- 调用异步函数的时候需要使用 await 关键字,使得这个异步调用拥有了挂起等待恢复的语义。
async/await 解决了怎样的问题
在 Swift 5.5 以前,getImageData 的实现通常依赖回调来实现结果的返回:
static func getImageData(url: String,
onSuccess: @escaping (Data) -> Void,
onError: @escaping (Error) -> Void) {
AF.request(url).responseData { response in
switch response.result {
case .success(let data):
onSuccess(data)
case .failure(let error):
onError(error)
}
}
}
很自然地,我们如果想要调用这个函数,代码写出来就像下面这样:
GitHubApi.getImageData(
url: avatar_url,
onSuccess: { data in
…
},
onError: { error in
…
})
那如果我想要在回调当中再触发一些其他的异步操作,结果会怎样呢?
GitHubApi.getImageData(
url: avatar_url,
onSuccess: { data in
…
cropImage(
onSuccess: { croppedImage in
saveImage(
onSuccess: {
…
},
onError: {
…
})
},
onError: {
…
})
},
onError: { error in
…
})
不难发现,随着逻辑复杂度的增加,代码的缩进会越来越深,可维护性也越来越差。
但这段代码如果用 async/await 改造一下,结果会怎样呢?
do {
let data = await GitHubApiAsync.getImageData(url: userItem.user.avatar_url)
let croppedImage = await cropImage(data)
await saveImage(croppedImage)
} catch {
…
}
与 getImageData 函数的同步版本相比,onSuccess 和 onError 这两个回调没有了。尽管结果仍然是异步返回的,但写起来却像是同步返回的一样。这样看来,运用 async/await 可以使回调的层级变少,从而使得代码逻辑变得更清晰。
延伸阅读:
二、Task 的创建
异步函数的关键在于 Continuation。所以,只要调用异步函数的位置能让异步函数获取到 Continuation,那么调用异步函数的问题就解决了。Swift 标准库提供了 Task 类来提供这个能力。
我们给出 Task 的构造器的定义:
public init(
priority: _Concurrency.TaskPriority? = nil,
operation: @escaping @Sendable () async -> Success)
public init(
priority: _Concurrency.TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success)
它接收一个异步闭包作为参数,创建一个 Task 实例并运行这个异步闭包。而在这个闭包当中,我们就可以调用任意异步函数了:
Task {
let result = await helloAsync()
print(result)
}
除了直接构造 Task 之外,还可以调用 Task 的 detach 函数来创建一个不一样的 Task:
Task.detached (operation: {
await helloAsync()
})
这个函数返回的也是一个 Task 实例,我们不妨看一下它的定义:
public static func detached(
priority: _Concurrency.TaskPriority? = nil,
operation: @escaping @Sendable () async -> Success
) -> _Concurrency.Task<Success, Failure>
public static func detached(
priority: _Concurrency.TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> Success
) -> _Concurrency.Task<Success, Failure>
注意到它其实是 Task 的静态函数,返回值正是 Task 类型。