A Coroutine Cheat Sheet

Igor Sorokin (srk1nn)
ITNEXT
Published in
4 min readMay 4, 2024

--

A coroutine is not a new technology, it was coined in 1958 by Melvin Conway. However, today, this term has become widely popular. Let’s discuss what a coroutine is and when it is useful.

Photo by Christina Morillo on Pexels, edited with Canva

Definition

Let’s discuss regular function first. A function begins execution at the start, and once it exits, it is finished. A coroutine extends function — it can be suspended at any time of execution and resumed later. To understand, consider the example

// Possible asymmetric coroutine API in Swift

let coro = coroutine { ctx in
// Role: callee

print(1)
ctx.suspend()
print(3)
}

// Role: caller

coro.resume()
print(2)
coro.resume()

Initially, we create a coroutine and store it in the coro variable. Then we call the resume method and the coroutine starts executing. The caller waits until the coroutine suspends or completes. The coroutine prints 1 and suspends, so control flow transfers to the caller. Later, the caller calls resume again and the coroutine resumes its execution from the point where it was suspended last time. The coroutine prints 3 and finishes execution, transferring control flow back to the caller.

The result in the console would be 1 2 3.

Use cases

A coroutine is a general concept that can be used to implement other high-level tools.

Cooperative multitasking

Coroutines achieve cooperative multitasking by releasing the CPU voluntarily. Each coroutine can pause itself in favor of another one when that’s the best thing to do for the set of tasks at hand. So, coroutines switch by cooperating, not competing with each other.

This approach consumes fewer resources (like memory and kernel objects) and uses fewer thread context switches, which increases performance. It is used in Swift async / await, Kotlin coroutines, Go goroutines and others.

Producer-consumer pattern

One specific example of a producer-consumer pattern is an Iterator. It produces elements to be consumed by the loop body. So an iterator can be implemented using a coroutine. A consumer asks an iterator next element using resume. When an iterator is ready, it suspends itself so the caller can handle the element. This can continue until an iterator reaches the last element.

Another example is a stream parser, whose principle is similar to iterators.

Other use cases could be event loops, infinite lists, pipes.

Stackful and Stackless

Coroutines distinguish between stackful and stackless based on where they store data (local variables, temporary calculations, etc.).

Stackless coroutine suspends execution by returning to the caller and the data that is required to resume execution is stored separately from the stack.

Stackful coroutine suspends execution by a context switch and the data is stored directly on the stack. Thus a stackful coroutine has separate stack memory. But what is a context switch? Let’s look at an execution from the CPU perspective.

The CPU runs code at the instruction pointer (ip register) on the call stack (sp register). Along with this, the CPU can use other registers to store temporary information. Therefore the state of execution at a certain point is the CPU snapshot (i.e. values in registers), let’s call this Execution Context.

On resume coroutine saves the caller execution context and applies the coroutine’s execution context to the CPU. So a control flow transfers to a coroutine from the caller. On suspend a coroutine makes reverse operation.

Context switch in stackful coroutine (whimsical.com)

Asymmetric and Symmetric

Another part, I want to mention is control transfer. A well-known classification of coroutines concerns the control-transfer operations that are provided and distinguishes the concepts of symmetric and asymmetric coroutines [3].

Asymmetric coroutine mechanisms provide two control-transfer operations: resume for invoking a coroutine and suspend for suspending it, the latter returning control to the coroutine invoker [3].

Symmetric coroutines have only one function that suspends the current context and resumes another coroutine [3]. It can not switch back to the caller, so there’s no caller-callee relationship. Sometimes you have to specify which symmetric coroutine has to be resumed next.

ctx.suspend_to(nextCoroutine)

One of the main differences is that an asymmetric coroutine can either call another coroutine or suspend itself by yielding control to its caller, while a symmetric coroutine may yield control to any other coroutine without restriction [3].

At the end

Thanks for reading, I hope this article will be helpful!

Make a clap and add this cheat sheet to bookmarks. Let me know, if I missed something.

Links

--

--