许多语言,如 Kotlin、Go、JavaScript、Python、Rust、C#、C++ 等,已经支持 协程,这使得 async/await 模式实现成为可能。Swift 目前还不支持此功能,但可以通过框架实现改进,无需更改语言。
主要特点
动机
异步编程通常与回调相关联。在回调数量不多时,这种方法相对方便。然而,当回调堆积如山并开始嵌套时,这被称为 恶意结构 或甚至是 回调地狱。
异步编程的另一个问题是 错误处理,因为 Swift 的天然错误处理机制无法使用。
关于 Rx 和其他此类框架呢?
有许多其他框架可以轻松使用异步代码,例如 Combine、RxSwift、PromiseKit 等。它们使用不同的方法,但也有一些缺点:
- 与回调类似,您也需要创建链式调用,这使得您通常无法使用循环、异常处理等。
- 通常,您需要学习一个包含数百个方法的复杂新 API。
- 直接处理实际数据,需要始终与一些包装器进行操作。
- 错误的链式调用的处理可能非常复杂。
Async/await
async/await 模式是一种替代方案,它允许异步、非阻塞函数以类似于普通同步函数的方式被结构化。
它在其他编程语言中已经非常成熟,是异步编程的一个发展。这种模式能够实现得益于协程。
让我们看一个在协程内部的例子,其中 await()
暂停它,直到结果可用时再恢复,而不会阻塞线程。
//executes coroutine on the main thread
DispatchQueue.main.startCoroutine {
//extension that returns CoFuture<(data: Data, response: URLResponse)>
let dataFuture = URLSession.shared.dataTaskFuture(for: imageURL)
//await CoFuture result that suspends coroutine and doesn't block the thread
let data: Data = try dataFuture.await().data
//create UIImage from the data
guard let image = UIImage(data: data) else { return }
//execute heavy task on global queue and await the result without blocking the thread
let thumbnail: UIImage = try DispatchQueue.global().await { image.makeThumbnail() }
//set image in UIImageView on the main thread
self.imageView.image = thumbnail
}
文档
要求
- 仅支持64位架构
- iOS 10+ / macOS 10.12+ / Ubuntu
- Xcode 10.4+
- Swift 5.2+
安装
- 它可通过 Swift Package Manager 在iOS、macOS和Linux上使用。
- 它可通过 CocoaPods 在iOS和macOS上使用。
使用 SwiftCoroutine
协程
协程是一种可以暂停并在稍后恢复,而不会阻塞线程的计算。协程建立在常规函数之上,并且可以在任何调度器上执行,并且在执行期间可以切换它们。
主要优点
- 暂停而非阻塞。协程的主要优点是在某一点暂停其执行而不阻塞线程,稍后继续。
- 快速上下文切换。协程之间的切换比线程之间的切换要快得多,因为它不需要操作系统的介入。
- 以同步方式执行异步代码。协程的使用允许以类似普通同步函数的方式构建异步、非阻塞函数。虽然协程可以在多个线程上运行,但您的代码看起来仍然一致,因此更容易理解。
使用方法
协程API的设计尽可能简约。它由描述如何调度协程的CoroutineScheduler
协议组成(DispatchQueue
已经符合它),以及带有实用方法的Coroutine
结构。此API足以完成惊人的工作。
以下示例展示了在协程中使用await()
来包装异步调用。
//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {
//await URLSessionDataTask response without blocking the thread
let (data, response, error) = try Coroutine.await { callback in
URLSession.shared.dataTask(with: url, completionHandler: callback).resume()
}
. . . use response on the main thread . . .
}
以下是我们将NSManagedObjectContext
符合到CoroutineScheduler
以在它上启动协程的方法。
extension NSManagedObjectContext: CoroutineScheduler {
func scheduleTask(_ task: @escaping () -> Void) {
perform(task)
}
}
//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {
let context: NSManagedObjectContext //context with privateQueueConcurrencyType
let request: NSFetchRequest<NSDictionary> //some complex request
//execute request on the context without blocking the main thread
let result: [NSDictionary] = try context.await { try context.fetch(request) }
}
未来与承诺
未来是一个只读的持有器,它将提供稍后的结果,而承诺是这个结果提供者。它们代表了异步操作最终完成或失败的情况。
未来和承诺的方法本身已成为行业标准。这是一个方便的机制来同步异步代码。但是,与协程一起,它将异步代码的使用提升到了新的水平,并已成为异步/等待模式的组成部分。如果协程是骨架,那么未来和承诺就是其肌肉。
主要功能
- 性能。它比大多数其他 futrures 和 promises 实现快得多。
- 可等待。您可以在协程内部等待结果。
- 可取消。您可以取消整个链式操作,并且可以处理它并完成相关动作。
使用方法
Futures 和 promises 通过相应的 CoFuture
类及其 CoPromise
子类表示。
//wraps some async func with CoFuture
func makeIntFuture() -> CoFuture<Int> {
let promise = CoPromise<Int>()
someAsyncFunc { int in
promise.success(int)
}
return promise
}
允许并行启动多个任务,稍后使用 await()
进行同步。
//create CoFuture<Int> that takes 2 sec. from the example above
let future1: CoFuture<Int> = makeIntFuture()
//execute coroutine on the global queue and returns CoFuture<Int> with future result
let future2: CoFuture<Int> = DispatchQueue.global().coroutineFuture {
try Coroutine.delay(.seconds(3)) //some work that takes 3 sec.
return 6
}
//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {
let sum: Int = try future1.await() + future2.await() //will await for 3 sec.
self.label.text = "Sum is \(sum)"
}
将 CoFuture
转换或组合成一个新的非常简单。
let array: [CoFuture<Int>]
//create new CoFuture<Int> with sum of future results
let sum = CoFuture { try array.reduce(0) { try $0 + $1.await() } }
通道
Futures 和 promises 提供了一种方便的方式在协程之间传递单个值。 Channels 提供了一种传输值流的方式。从概念上讲,通道类似于一个允许在接收为空时挂起协程,或者在发送为满时挂起的队列。
这种非阻塞原语在 Go 和 Kotlin 等语言中广泛使用,它也是提高协程工作的另一种工具。
使用方法
要创建通道,请使用 CoChannel
类。
//create a channel with a buffer which can store only one element
let channel = CoChannel<Int>(capacity: 1)
DispatchQueue.global().startCoroutine {
for i in 0..<100 {
//imitate some work
try Coroutine.delay(.seconds(1))
//sends a value to the channel and suspends coroutine if its buffer is full
try channel.awaitSend(i)
}
//close channel when all values are sent
channel.close()
}
DispatchQueue.global().startCoroutine {
//receives values until closed and suspends a coroutine if it's empty
for i in channel.makeIterator() {
print("Receive", i)
}
print("Done")
}
作用域
通常不需要引用已启动的协程、CoFuture
和 CoChannel
。它们在执行后会被销毁。但经常需要提前完成它们,当它们不再需要时。为此,CoFuture
和 CoChannel
方法用于取消。
CoScope
使管理这些对象的生命周期变得简单。它允许您保留它们的弱引用,并在必要时或销毁时取消。
使用
您可以将协程、CoFuture
、CoChannel
和其他CoCancellable
添加到CoScope
中,当它们不再需要或进行反初始化时进行取消。
class ViewController: UIViewController {
let scope = CoScope() //will cancel all objects on `cancel()` or deinit
func performSomeWork() {
//create new `CoChannel` and add to `CoScope`
let channel = makeSomeChannel().added(to: scope)
//execute coroutine and add to `CoScope`
DispatchQueue.main.startCoroutine(in: scope) { [weak self] in
for item in channel.makeIterator() {
try self?.performSomeWork(with: item)
}
}
}
func performSomeWork(with item: Item) throws {
//create new `CoFuture` and add to `CoScope`
let future = makeSomeFuture(item).added(to: scope)
let result = try future.await()
. . . do some work using result . . .
}
}