Swift Coroutine

Beta testing. Unit tests and documentation in progress. Possible minor changes in API.

macOS Ubuntu

Many languages, such as Kotlin, JavaScript, Go, Rust, C++, and others, already have coroutines support that makes the use of asynchronous code easier. This feature is not yet supported in Swift, but this can be improved by a framework without the need to change the language.

This is the first implementation of coroutines for Swift with iOS, macOS and Linux support. They make the async/await pattern implementation possible. In addition, the framework includes futures and promises for more flexibility and ease of use. All this allows to do things that were not possible in Swift before.

Usage

This is an example of a combined usage of coroutines with futures and promises.

//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {

    //extension that returns CoFuture<(data: Data, response: URLResponse)>
    let dataFuture = URLSession.shared.dataTaskFuture(for: imageURL)

    //await result that suspends coroutine and doesn't block the thread
    let data = try dataFuture.await().data

    //create UIImage from data or throw the error
    guard let image = UIImage(data: data) else { throw URLError(.cannotParseResponse) }

    //execute heavy task on global queue and await the result without blocking the thread
    let thumbnail = try DispatchQueue.global().await { 
        image.makeThumbnail() //some method that returns UIImage
    }

    //set image in UIImageView on the main thread
    self.imageView.image = thumbnail

}

Requirements

  • iOS 11.0+ / macOS 10.13+ / Ubuntu 18.0+
  • Xcode 10.2+
  • Swift 5+

Installation

SwiftCoroutine is available through the Swift Package Manager for macOS and iOS.

Working with SwiftCoroutine

Async/await

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 callback hell.

The async/await pattern is an alternative. 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.

Key benefits

  • Suspend instead of block. The main advantage of coroutines is the ability to suspend their execution at some point without blocking a thread and resuming later on.
  • Fast context switching. Switching between coroutines is much faster than switching between threads as it does not require the involvement of operating system.
  • Asynchronous code in synchronous manner. The use of coroutines allows an asynchronous, non-blocking function to be structured in a manner similar to an ordinary synchronous function. And even though coroutines can run in multiple threads, your code will still look consistent and therefore easy to understand.

The coroutines API design is as minimalistic as possible. It consists of the CoroutineScheduler protocol, which requires to implement only one method, and the Coroutine structure with utility methods. This API is enough to do amazing things.

The CoroutineScheduler protocol describes how to schedule tasks and as an extension you get the startCoroutine() method for executing coroutines on it, as well as the await() method for awaiting the result of the task (that is executed on your scheduler) inside the coroutine without blocking the thread. The framework includes the implementation of this protocol for DispatchQueue, but you can easily add it for other schedulers.

Coroutine has static utility methods for usage inside coroutines, including the await() method which suspends and resumes it on callback. It allows you to easily wrap asynchronous functions to deal with them as synchronous.

Main features

  • Any scheduler. You can use any scheduler to execute coroutines, including standard DispatchQueue or even NSManagedObjectContext and MultiThreadedEventLoopGroup.
  • Await instead of resume/suspend. For convenience and safety, coroutines’ resume/suspend has been replaced by await, which suspends it and resumes on callback.
  • Lock-free await. Await is implemented using atomic variables. This makes it especially fast in cases where the result is already available.
  • Memory efficiency. Contains a mechanism that allows to reuse stacks and, if necessary, effectively store their contents with minimal memory usage.
  • Create your own API. Gives you a very flexible tool to create own powerful add-ons or easily integrate it with existing solutions.

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

func awaitThumbnail(url: URL) throws -> UIImage {
    //await URLSessionDataTask response without blocking the thread
    let (data, _, error) = try Coroutine.await {
        URLSession.shared.dataTask(with: url, completionHandler: $0).resume()
    }

    //parse UIImage or throw the error
    guard let image = data.flatMap(UIImage.init)
        else { throw error ?? URLError(.cannotParseResponse) }

    //execute heavy task on global queue and await its result
    return try DispatchQueue.global().await { image.makeThumbnail() }
}

func setThumbnail(url: URL) {
    //execute coroutine on the main thread
    DispatchQueue.main.startCoroutine {

        //await image without blocking the thread
        let thumbnail = try? self.awaitThumbnail(url: url)

        //set image on the main thread
        self.imageView.image = thumbnail
    }
}

Here’s how we can conform NSManagedObjectContext to 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<Entity> //some complex request

    //execute request without blocking the main thread
    let result = try context.await { try context.fetch(request) }
}

Futures and Promises

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

Futures and promises are represented by the corresponding CoFuture class and its CoPromise subclass. CoFuture is a holder for a result that will be provided later.

Main features

  • Best performance. It is much faster than most of other futures and promises implementations.
  • Build chains. With flatMap() and map(), you can create data dependencies via CoFuture chains.
  • Cancellable. You can cancel the whole chain as well as handle it and complete the related actions.
  • Awaitable. You can await the result inside the coroutine.
  • Combine-ready. You can create Publisher from CoFuture, and vice versa make CoFuture a subscriber.

Here is an example of URLSession extension to creating CoFuture for URLSessionDataTask. The example of using it with coroutines and await() is provided here.

extension URLSession {

    typealias DataResponse = (data: Data, response: URLResponse)

    func dataTaskFuture(for urlRequest: URLRequest) -> CoFuture<DataResponse> {
        //create CoPromise that is a subclass of CoFuture for delivering the result
        let promise = CoPromise<DataResponse>()

        //create URLSessionDataTask
        let task = dataTask(with: urlRequest) {
            if let error = $2 {
                promise.fail(error)
            } else if let data = $0, let response = $1 {
                promise.success((data, response))
            } else {
                promise.fail(URLError(.badServerResponse))
            }
        }
        task.resume()

        //handle CoFuture canceling to cancel URLSessionDataTask
        promise.whenCanceled(task.cancel)

        return promise
    }

}

Also CoFuture allows to start multiple tasks in parallel and synchronize them later with await().

//execute task on the global queue and returns CoFuture<Int> with deferred result
let future1: CoFuture<Int> = DispatchQueue.global().coFuture {
    try Coroutine.delay(.seconds(2)) //some work that takes 2 sec.
    return 5
}

let future2: CoFuture<Int> = DispatchQueue.global().coFuture {
    try Coroutine.delay(.seconds(3)) //some work that takes 3 sec.
    return 6
}

//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {
    let sum = try future1.await() + future2.await() //will await for 3 sec.
    self.label.text = "Sum is \(sum)"
}

Apple has introduced a new reactive programming framework Combine that makes writing asynchronous code easier and includes a lot of convenient and common functionality. We can use it with coroutines by making CoFuture a subscriber and await its result.

//create Combine publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url).map(\.data)

//execute coroutine on the main thread
DispatchQueue.main.startCoroutine {
    //subscribe CoFuture to publisher
    let future = publisher.subscribeCoFuture()

    //await data without blocking the thread
    let data = try future.await()
}