Coroutines

Lecture



Coroutine (from English coroutine) — a program module organized in a special way to provide interaction with other modules on the principle of cooperative multitasking: the module suspends at a certain point, saving its complete state (including the call stack and instruction pointer), and passes control to another one, which, in turn, performs its task and passes control back, saving its own stack and instruction pointer. Along with fibers (English fiber), coroutines are a means of providing “lightweight” program multithreading in the sense that they can be implemented without using operating-system context-switching mechanisms.

Coroutines are more flexible and generalized than subroutines: compared to a subroutine, which always has a single entry point, a coroutine has a starting entry point, after which a sequence of returns is placed inside it; each return is also endowed with its own entry point. A subroutine can return only once, a coroutine can return control several times. The runtime of a subroutine is determined by the LIFO principle (the last subroutine called finishes first), whereas the runtime of a coroutine is determined by the need for it to run.

Coroutines

The emergence of the concept of the coroutine is attributed to a construction used by Melvin Conway in 1958 in the practice of assembly-language programming ; in the 1960s — 1970s coroutines were used in some high-level languages (CLU, Simula, Modula-2), but gained noticeable spread only in the 2000s, when numerous coroutine-support libraries appeared in popular programming languages. In some newer languages (such as Lua, Ruby, Go, Julia) coroutine-support libraries are built in from the start. Coroutines are used to implement many similar program components, such as generators and iterators, infinite lists using lazy evaluation, channels, finite state machines inside a single subroutine (where the state is determined by the current entry and exit point), implementations of exception handling, and the actor model.

General use

Coroutines are useful for implementing the following:

  • Finite state machines inside a single subroutine, where the state is determined by the current entry/exit point of the procedure; this can lead to more readable code compared to using goto , and can also be implemented via mutual recursion with tail calls .
  • The actor model of concurrency, for example, in video games . Each actor has its own procedures (this again logically separates the code), but they voluntarily pass control to a central scheduler, which executes them sequentially (this is a form of cooperative multitasking ).
  • Generators , which are useful for streams – especially I/O – and for the general traversal of data structures.
  • Communicating sequential processes , where each subprocess is a coroutine. Channel input/output and blocking operations create coroutines, and the scheduler unblocks them on completion events. Alternatively, each subprocess can be the parent of the one following it in the data pipeline (or of the one preceding it, in which case the pattern can be expressed as nested generators).
  • Backtracking, commonly used in mathematical software, where a routine such as a solver, an integral evaluator... requires the use of a process to perform calculations, such as evaluating an equation or an integrand.

Implementations

A significant portion of popular programming languages, including C and its derivatives (C++ prior to C++20), do not have direct support for coroutines in the language or the standard library (this is due, for the most part, to the requirements of the stack-based implementation of subroutines).

In a situation where coroutines, as a natural way of implementing components, are unavailable, a typical solution is to create coroutines using a set of boolean flags and other state variables to maintain external state between calls. Conditions inside the code lead to the execution of different instruction sequences on successive calls according to the values of the state variables. Another typical solution is to implement a finite state machine manually using a large switch statement. Such implementations are difficult to maintain and support.

Threads are a suitable alternative to coroutines in most modern development. Threads provide capabilities for managing the interaction of pieces of code executing “simultaneously”. Therefore this is a solution to large and complex problems, it includes powerful, comprehensive capabilities and has an accompanying learning complexity. However, despite other alternatives, threads are widely available in the C environment, are familiar to most programmers, and are usually implemented, documented and maintained.

Some attempts to implement coroutines in C:

  • Coroutines by Simon Tatham using “Duff's device” ;
  • PCL (portable coroutine library) ;
  • Coro ;
  • the Boost.Coroutine library from the Boost collection.

One of the approaches used to implement coroutines in languages without built-in support for them — stackless protothreads, which provide a blocking context at the cost of a few bytes of memory per thread.

Functional programming languages often implement coroutines, for example, Scheme, Lisp, Haskell. In a number of languages, built-in support for coroutines was added in later releases, such as Python (starting with 2.5, and with explicit syntactic support starting with 3.5), PHP (starting with 5.5), Kotlin (from version 1.1), JavaScript (from version 1.7), C# (from version 2.0), Tcl (from version 8.6).

Coroutines
Coroutines are more generalized than subroutines. A distinguishing feature of coroutines is the presence of several entry points, as depicted in figure 5. A subroutine always has a single entry point, a coroutine has a starting entry point and a sequence of returns and the entry points following them placed inside it. A subroutine can return only once, a coroutine can return control several times. After a subroutine is called again, execution starts from the starting entry point, whereas in a coroutine it starts from the place where control was passed.

Coroutines
Figure 5 - Distinguishing features of a coroutine compared to a subroutine.


Thanks to coroutines, the code looks as though it runs in a single thread of execution. As a result, ease of writing programs, ease of maintenance and bug catching are increased. It is worth noting that processor time is spent on creating and managing coroutines, so performance is 10%-15% lower than with a purely asynchronous approach .
There are two different implementations of coroutines:

  1. The StackLess implementation, which is essentially a finite state machine storing the necessary local variables and entry points. The Async/await construct of C# version 5 is implemented exactly this way [10].
  2. The StackFull implementation works by saving/restoring all processor registers and the stack respectively — essentially, like real threads, except all without going through the OS, so it is practically instantaneous. This implementation is used in the Boost.Coroutine, Boost.Context library of the C++ language .

StackFull is a more performant implementation of coroutines, but also the most complex. This implementation exists only in the Boost.Coroutine, Boost.Context libraries of the C++ language. The other programming languages use the StackLess implementation.
Effective use of asynchronous operations
Asynchronous operations are the most effective method of increasing the performance of a dispatch center, which spends most of its time on I/O operations. This method was tested by me on a dispatch-center project for a high-precision object-positioning system. The number of clients whose requests are handled by the server increased by 40%. This is a very significant performance gain with minimal labor costs. The correct choice of programming language contributed to this. In this case it is C#, which in its fifth version gained support for asynchronous operations by means of coroutines.

See also

  • Async/await
  • Pipeline — a kind of coroutine used for communication between programs
  • Protothreads — a lightweight, stackless thread implementation using a mechanism similar to a coroutine.

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Highly loaded projects. Theory of parallel computing. Supercomputers. Distributed systems"

Terms: Highly loaded projects. Theory of parallel computing. Supercomputers. Distributed systems