SwiftCoroutine 2.1.11

SwiftCoroutine 2.1.11

Alex Belozierov 维护。



  • 作者:
  • Alex Belozierov

Swift Coroutine

macOS Ubuntu codecov codebeat badge

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

主要特点

  • 这是第一个支持 iOS、macOS 和 Linux 的 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
    
}

文档

API 文档

要求

  • 仅支持64位架构
  • iOS 10+ / macOS 10.12+ / Ubuntu
  • Xcode 10.4+
  • Swift 5.2+

安装

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

作用域

通常不需要引用已启动的协程、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 . . .
    }

}