Swift Coroutine

macOS Ubuntu codecov codebeat badge

许多语言,例如 Kotlin、Go、JavaScript、Python、Rust、C#、C++ 等,已经支持协程,这使得 async/await 模式的实现成为可能。Swift 尚未支持此功能,但可以通过框架来改进,而无需更改语言本身。

主要特性

动机

异步编程通常与回调相关联。 在回调不多时,这很方便,但当回调太多并开始嵌套时,就会出现问题。 这被称为 末日金字塔 (pyramid of doom) 甚至 回调地狱 (callback hell)

异步编程的另一个问题是 错误处理,因为无法使用 Swift 原生的错误处理机制。

Rx 和其他此类框架怎么样?

还有许多其他框架可以轻松使用异步代码,例如 Combine、RxSwift、PromiseKit 等。 它们使用其他方法,但存在一些缺点:

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
    
}

文档

API 文档

要求

安装

使用 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) }
}

Futures 和 Promises

Future 是一个只读的占位符,用于存储稍后将提供的结果,而 Promise 是该结果的提供者。 它们表示异步操作的最终完成或失败。

Futures 和 Promises 方法本身已成为行业标准。 它是一种同步异步代码的便捷机制。 但是与协程一起使用,它将异步代码的使用提升到一个新的水平,并成为 async/await 模式的一部分。 如果协程是骨骼,那么 Futures 和 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 转换为或组合成一个新的 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() } }

Channels

Futures 和 Promises 提供了一种在协程之间传输单个值的便捷方法。 Channels 提供了一种传输值流的方法。 从概念上讲,Channel 类似于队列,如果队列为空,则允许暂停接收的协程;如果队列已满,则允许暂停发送的协程。

这种非阻塞原语广泛用于 Go 和 Kotlin 等语言,它是另一种改进协程工作方式的工具。

用法

要创建 Channels,请使用 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")
}

Scope

所有启动的协程、CoFutureCoChannel 通常不需要被引用。 它们在执行后会被释放。 但是,通常需要在它们不再需要时提前完成它们。 为此,CoFutureCoChannel 具有用于取消的方法。

CoScope 使管理这些对象的生命周期更加容易。 它允许您保持对它们的弱引用,并在必要时或在释放时取消它们。

用法

您可以将协程、CoFutureCoChannel 和其他 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 . . .
    }

}