许多语言,如 Kotlin、Go、JavaScript、Python、Rust、C#、C++ 等,已经具有 协程 支持,这使得 async/await 模式实现成为可能。这种特性在 Swift中还尚未支持,但可以通过无需改变语言的框架来改进。
主要功能
- 这是首个为 iOS、macOS 和 Linux 支持的 Swift 协程 实现。
- 它包括了 future 和 channel,这些都补充了协程,增加了更多灵活性。
- 它是完全 无锁 并且只使用 原子 原语来进行同步。
动机
异步编程通常与回调相关联。这非常方便,直到回调太多并且开始嵌套。然后它被称为 死神金字塔 或甚至 回调地狱。
异步编程的另一个问题是 错误处理,因为 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) }
}
将来的和承诺
未来是一个只读的持有器,用于保留稍后提供的结果,而承诺是实现这个结果的提供者。它们代表了异步操作最终的成功或失败。
《未来和承诺》方法本身已成为行业标准。这是一种方便的机制,用于同步异步代码。但与协程结合使用时,它将异步代码的使用提升到了一个新的层次,并成为了async/await模式的组成部分。如果协程是骨架,那么未来和承诺就是它的肌肉。
主要特性
- 性能。它比大多数其他未来和承诺实现都要快得多。
- 可暂停执行。你可以在协程内部等待结果。
- 可取消。你可以取消整个链,也可以处理它并完成相关操作。
用法
未来和承诺由相应的《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() } }
通道
未来和承诺提供了一种方便的方式,在协程之间传输单个值。《a href="https://zh.wikipedia.org/wiki/Channel_(programming)" rel="nofollow">通道提供了一种传输值流的方法。从概念上看,通道类似于一个队列,它允许在接收到空时挂起协程,或者在发送时接收到满时挂起。
这种非阻塞原语在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 . . .
}
}