SwiftCoroutine Reference (original) (raw)

Swift Coroutine

macOS Ubuntu codecov codebeat badge

Many languages, such as Kotlin, Go, JavaScript, Python, Rust, C#, C++ and others, already have coroutines support that makes the async/await pattern implementation possible. This feature is not yet supported in Swift, but this can be improved by a framework without the need to change the language.

Main features

Motivation

Asynchronous programming is usually associated with callbacks. It is quite convenient until there are too many of them and they start nesting. Then it’s called a pyramid of doom or even callback hell.

Another problem of asynchronous programming is error handling, because Swift’s natural error handling mechanism cannot be used.

What about Rx and other such frameworks?

There are many other frameworks that make it easy to use asynchronous code, such as Combine, RxSwift, PromiseKit and so on. They use other approaches that have some drawbacks:

Async/await

The async/await pattern is an alternative that allows an asynchronous, non-blocking function to be structured in a way similar to an ordinary synchronous function.

It is already well-established in other programming languages and is an evolution in asynchronous programming. The implementation of this pattern is possible thanks to coroutines.

Let’s have a look at the example with coroutine inside of which await() suspends it and resumes when the result is available without blocking the thread.

//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

}

Documentation

API documentation

Requirements

Installation

Working with SwiftCoroutine

Coroutines

A coroutine is a computation that can be suspended and resumed at a later time without blocking a thread. Coroutines build upon regular functions and can be executed on any scheduler with a possibility to switch among them during execution.

Key benefits

Usage

The coroutines API design is as minimalistic as possible. It consists of the [CoroutineScheduler](Protocols/CoroutineScheduler.html) protocol that describes how to schedule coroutines (DispatchQueue already conforms it), and the [Coroutine](Structs/Coroutine.html) structure with utility methods. This API is enough to do amazing things.

The following example shows the usage of await() inside a coroutine to wrap asynchronous calls.

//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 . . . 
}

Here’s how we can conform NSManagedObjectContext to [CoroutineScheduler](Protocols/CoroutineScheduler.html) for launching coroutines on it.

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 and Promises

A future is a read-only holder for a result that will be provided later and the promise is the provider of this result. They represent the eventual completion or failure of an asynchronous operation.

The futures and promises approach itself has become an industry standart. It is a convenient mechanism to synchronize asynchronous code. But together with coroutines, it takes the usage of asynchronous code to the next level and has become a part of the async/await pattern. If coroutines are a skeleton, then futures and promises are its muscles.

Main features

Usage

Futures and promises are represented by the corresponding [CoFuture](Classes/CoFuture.html) class and its [CoPromise](Classes/CoPromise.html) subclass.

//wraps some async func with CoFuture
func makeIntFuture() -> CoFuture<Int> {
    let promise = CoPromise<Int>()
    someAsyncFunc { int in
        promise.success(int)
    }
    return promise
}

It allows to start multiple tasks in parallel and synchronize them later with 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)"
}

It’s very easy to transform or compose [CoFuture](Classes/CoFuture.html)s into a new one.

let array: [CoFuture<Int>]

//create new CoFuture<Int> with sum of future results
let sum = CoFuture { try array.reduce(0) { try <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>0</mn><mo>+</mo></mrow><annotation encoding="application/x-tex">0 + </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">0</span><span class="mord">+</span></span></span></span>1.await() } }

Channels

Futures and promises provide a convenient way to transfer a single value between coroutines. Channels provide a way to transfer a stream of values. Conceptually, a channel is similar to a queue that allows to suspend a coroutine on receive if it is empty, or on send if it is full.

This non-blocking primitive is widely used in such languages as Go and Kotlin, and it is another instrument that improves working with coroutines.

Usage

To create channels, use the [CoChannel](Classes/CoChannel.html) class.

//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

All launched coroutines, [CoFuture](Classes/CoFuture.html)s and [CoChannel](Classes/CoChannel.html)s, usually do not need to be referenced. They are deinited after their execution. But often there is a need to complete them earlier, when they are no longer needed. For this, [CoFuture](Classes/CoFuture.html) and [CoChannel](Classes/CoChannel.html) have methods for canceling.

[CoScope](Classes/CoScope.html) makes it easier to manage the life cycle of these objects. It allows you to keep weak references to them and cancel if necessary or on deinit.

Usage

You can add coroutines, [CoFuture](Classes/CoFuture.html)s, [CoChannel](Classes/CoChannel.html)s and other [CoCancellable](Protocols/CoCancellable.html) to [CoScope](Classes/CoScope.html) to cancel them when they are no longer needed or on deinit.

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 . . .
    }

}