在今年的WWDC大会上,Swift 5.5的新并发功能套件无疑发挥了重要作用。特别是,新引入的async/await模式不仅可以在以Swift为重点的会议和公告中看到,而且在会议上公布的新API和功能中都可以看到。
虽然async/await很有可能成为未来在苹果平台上编写异步代码的主要方式,但就像所有重大的技术转变一样,我们还需要一段时间才能达到这个目标。因此,在这篇文章中,让我们探讨一些方法来 "弥补 "async/await的新世界和其他类型的异步Swift代码之间的差距。
什么是 async/await?
让我们先快速回顾一下async/await到底是什么。随着我对这种新模式以及它被整合到苹果SDK中的方式获得更多的实践经验,我将在未来的文章中详细介绍,但在最基本的层面上,async/await使我们能够用async 关键字来注释异步函数(或计算属性),这反过来要求我们在调用它们时使用await 关键字。在这一点上,系统会自动管理所有涉及到等待这种异步调用完成的复杂性,而不会阻断任何其他的、外部的代码执行。
例如,下面的DocumentLoader 有一个async-markedloadDocument 方法,它使用Foundation的URLSession API,通过等待从指定URL下载的数据来执行异步网络调用:
struct DocumentLoader {
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func loadDocument(withID id: Document.ID) async throws -> Document {
let url = urlForForLoadingDocument(withID: id)
let (data, _) = try await urlSession.data(from: url)
return try decoder.decode(Document.self, from: data)
}
...
}
所以async-marked函数可以反过来调用其他异步函数,只需在这些调用前加上await 关键字。在这一点上,本地的执行将被暂停,直到那个嵌套的await 完成,这期间也不会阻挡任何其他代码的执行。
从同步上下文中调用异步函数
但问题是--我们如何从一个本身不是异步的上下文中调用一个async-标记的函数?例如,如果我们想在诸如基于UIKit的视图控制器中使用上述DocumentLoader 呢?这就是任务的作用。我们需要做的是将我们对loadDocument 的调用包裹在一个Task ,在其中我们可以执行我们的异步调用--像这样:
class DocumentViewController: UIViewController {
private let documentID: Document.ID
private let loader: DocumentLoader
...
private func loadDocument() {
Task {
do {
let document = try await loader.loadDocument(withID: documentID)
display(document)
} catch {
display(error)
}
}
}
private func display(_ document: Document) {
...
}
private func display(_ error: Error) {
...
}
}
上述模式真正巧妙的地方在于,我们现在可以使用 Swift 默认的do/try/catch 错误处理机制,即使是在执行异步调用时。我们也不再需要做任何形式的 "弱自舞 "以避免保留周期,我们甚至不需要在主队列上手动分配我们的UI更新,因为现在已经*由主角色*为我们处理了。
用async/await支持改造现有的API
接下来,让我们来看看如何走另一条路--即如何使我们现有的一些异步代码与新的async/await模式兼容。
作为一个例子,假设我们的应用程序包含以下CommentLoader 类型,让我们使用基于完成处理程序的API加载所有附加到给定文档的评论:
struct CommentLoader {
...
func loadCommentsForDocument(
withID id: Document.ID,
then handler: @escaping (Result<[Comment], Error>) -> Void
) {
...
}
}
最初,我们可能需要对上述API进行重大改变,以使其与异步/等待兼容,但实际上并非如此。我们真正要做的是使用新的withCheckedThrowingContinuation 函数,将对我们现有方法的调用包裹在一个async- 标记的版本中--就像这样:
extension CommentLoader {
func loadCommentsForDocument(
withID id: Document.ID
) async throws -> [Comment] {
try await withCheckedThrowingContinuation { continuation in
loadCommentsForDocument(withID: id) { result in
switch result {
case .success(let comments):
continuation.resume(returning: comments)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
请注意,传递到我们封装闭包中的continuation ,只能被调用一次。如果我们不小心调用了两次,就会导致一个致命的错误。在这种情况下,这种情况发生的可能性为零,因为我们的完成处理程序只被调用一次,但在使用这种技术时,这绝对是值得注意的事情。
有了上述内容,我们现在可以使用await 关键字轻松地调用我们的loadCommentsForDocument 方法,就像调用系统提供的异步API时一样。例如,这里我们可以更新我们的DocumentLoader ,现在可以自动获取它所加载的每个文档的评论。
struct DocumentLoader {
var commentLoader: CommentLoader
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func loadDocument(withID id: Document.ID) async throws -> Document {
let url = urlForForLoadingDocument(withID: id)
let (data, _) = try await urlSession.data(from: url)
var document = try decoder.decode(Document.self, from: data)
document.comments = try await commentLoader.loadCommentsForDocument(
withID: id
)
return document
}
}
async/await真正的好处是,即使我们增加了额外的、嵌套的调用,我们的代码也没有真正地增长多少复杂性。它看起来或多或少就像老式的同步代码,再加上一些await 的关键字。
适应单输出的Combine发布器
最后,让我们也来看看如何让某些由Combine驱动的代码也能兼容异步/等待。虽然 Swift 的新并发系统包括其他更类似于 "Combine "的方式,可以随着时间的推移发出动态值流(比如AsyncSequence ,以及即将推出的AsyncStream ),但如果我们想要做的只是等待来自 Combine 管道的单个异步结果,那么这里有一种方法可以让我们以一种相当轻量级的方式实现。
使用我们之前使用的基于continuation 的技术,让我们用一个singleResult 方法来扩展 Combine 的Publisher 协议,该方法将用给定发布者发出的第一个值来恢复我们的延续。我们还将使用 Swift 的闭合捕获机制来保留 Combine 订阅的AnyCancellable 实例,直到我们的操作完成--就像这样:
extension Publishers {
struct MissingOutputError: Error {}
}
extension Publisher {
func singleResult() async throws -> Output {
var cancellable: AnyCancellable?
var didReceiveValue = false
return try await withCheckedThrowingContinuation { continuation in
cancellable = sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
continuation.resume(throwing: error)
case .finished:
if !didReceiveValue {
continuation.resume(
throwing: Publishers.MissingOutputError()
)
}
}
},
receiveValue: { value in
guard !didReceiveValue else { return }
didReceiveValue = true
cancellable?.cancel()
continuation.resume(returning: value)
}
)
}
}
}
如果我们现在想象一下,我们之前使用的CommentLoader ,而是有一个由Combine驱动的API(而不是一个基于闭包的API),那么我们现在可以很容易地使用async/await来使用上述扩展来调用它:
struct CommentLoader {
...
func loadCommentsForDocument(
withID id: Document.ID
) -> AnyPublisher<[Comment], Error> {
...
}
}
...
let comments = try await loader
.loadCommentsForDocument(withID: documentID)
.singleResult()
当然,就像它的名字所暗示的那样,我们新的singleResult 方法将只返回一个给定的Combine发布器发出的第一个值,所以它应该只用于那些预计不会在一段时间内产生多个值的发布器(除非我们只对第一个值感兴趣)。
我们将在接下来的文章中仔细研究如何在Combine、async/await和Swift新并发系统的其他部分之间搭建桥梁。
总结
Async/await 为在 Swift 中编写异步代码提供了一种令人兴奋的新方法,并且很可能成为苹果公司未来整体 API 设计中非常关键的一部分。然而,由于它不能向后兼容旧的操作系统版本,而且我们很可能还需要与其他尚未使用async/await的代码进行交互,因此找到将这些代码与Swift的新并发系统连接起来的方法对许多团队来说将是非常重要的。
谢谢你的阅读!