139

What are coroutines in ?

In what ways it is different from "Parallelism2" or/and "Concurrency2" (look into below image)?

The below image is from ISOCPP.

https://isocpp.org/files/img/wg21-timeline-2017-03.png

enter image description here

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Pavan Chandaka
  • 11,671
  • 5
  • 26
  • 34
  • 3
    To answer "In what way is the concept of *coroutines* different from *parallelism* and *concurrency*?" -- https://en.wikipedia.org/wiki/Coroutine – Ben Voigt Apr 19 '17 at 18:44
  • related: http://stackoverflow.com/q/35121078/103167 – Ben Voigt Apr 19 '17 at 18:49
  • 4
    A very good and easy-to-follow intro to coroutine is James McNellis's presentation “Introduction to C++ Coroutines" (Cppcon2016). – philsumuru Apr 19 '17 at 18:52
  • 2
    Finally it would also be good to cover "How are *coroutines* in C++ different from other languages' implementations of coroutines and resumable functions?" (which the above-linked wikipedia article, being language agnostic, doesn't address) – Ben Voigt Apr 19 '17 at 18:55
  • 1
    who else read this "quarantine in C++20" ? – Sahib Yar Apr 13 '20 at 13:26
  • 2
    YouTube link to James McNellis's “Introduction to C++ Coroutines" at CppCon 2016: https://youtu.be/ZTqHjjm86Bw – Milan Apr 11 '22 at 16:01

3 Answers3

264

At an abstract level, Coroutines split the idea of having an execution state off of the idea of having a thread of execution.

SIMD (single instruction multiple data) has multiple "threads of execution" but only one execution state (it just works on multiple data). Arguably parallel algorithms are a bit like this, in that you have one "program" run on different data.

Threading has multiple "threads of execution" and multiple execution states. You have more than one program, and more than one thread of execution.

Coroutines has multiple execution states, but does not own a thread of execution. You have a program, and the program has state, but it has no thread of execution.


The easiest example of coroutines are generators or enumerables from other languages.

In pseudo code:

function Generator() {
  for (i = 0 to 100)
    produce i
}

The Generator is called, and the first time it is called it returns 0. Its state is remembered (how much state varies with implementation of coroutines), and the next time you call it it continues where it left off. So it returns 1 the next time. Then 2.

Finally it reaches the end of the loop and falls off the end of the function; the coroutine is finished. (What happens here varies based on language we are talking about; in python, it throws an exception).

Coroutines bring this capability to C++.

There are two kinds of coroutines; stackful and stackless.

A stackless coroutine only stores local variables in its state and its location of execution.

A stackful coroutine stores an entire stack (like a thread).

Stackless coroutines can be extremely light weight. The last proposal I read involved basically rewriting your function into something a bit like a lambda; all local variables go into the state of an object, and labels are used to jump to/from the location where the coroutine "produces" intermediate results.

The process of producing a value is called "yield", as coroutines are bit like cooperative multithreading; you are yielding the point of execution back to the caller.

Boost has an implementation of stackful coroutines; it lets you call a function to yield for you. Stackful coroutines are more powerful, but also more expensive.


There is more to coroutines than a simple generator. You can await a coroutine in a coroutine, which lets you compose coroutines in a useful manner.

Coroutines, like if, loops and function calls, are another kind of "structured goto" that lets you express certain useful patterns (like state machines) in a more natural way.


The specific implementation of Coroutines in C++ is a bit interesting.

At its most basic level, it adds a few keywords to C++: co_return co_await co_yield, together with some library types that work with them.

A function becomes a coroutine by having one of those in its body. So from their declaration they are indistinguishable from functions.

When one of those three keywords are used in a function body, some standard mandated examining of the return type and arguments occurs and the function is transformed into a coroutine. This examining tells the compiler where to store the function state when the function is suspended.

The simplest coroutine is a generator:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yield suspends the functions execution, stores that state in the generator<int>, then returns the value of current through the generator<int>.

You can loop over the integers returned.

co_await meanwhile lets you splice one coroutine onto another. If you are in one coroutine and you need the results of an awaitable thing (often a coroutine) before progressing, you co_await on it. If they are ready, you proceed immediately; if not, you suspend until the awaitable you are waiting on is ready.

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_data is a coroutine that generates a std::future when the named resource is opened and we manage to parse to the point where we found the data requested.

open_resource and read_lines are probably async coroutines that open a file and read lines from it. The co_await connects the suspending and ready state of load_data to their progress.

C++ coroutines are much more flexible than this, as they were implemented as a minimal set of language features on top of user-space types. The user-space types effectively define what co_return co_await and co_yield mean -- I've seen people use it to implement monadic optional expressions such that a co_await on an empty optional automatically propogates the empty state to the outer optional:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  co_return (co_await a) + (co_await b);
}

instead of

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 38
    This is one of the clearest explanations of what coroutines are that I've ever read. Comparing them to and distinguishing them from SIMD and classical threads was an excellent idea. – Omnifarious Feb 16 '18 at 15:30
  • Why is `r` `co_return`'d and not `co_yield`'d? Is that example really to only extract one line and then be done (as written), or should it be returning as many parsed lines as it contains, yielding each one until there are no more? – Mordachai Mar 26 '19 at 12:36
  • 1
    @mord yes it is supposed to return 1 element. Might need polishing; if we want more than one line need a different control flow. – Yakk - Adam Nevraumont Mar 26 '19 at 13:00
  • What's happening if I call a coroutine from different threads? – JulianW Mar 30 '19 at 14:54
  • @JulianH At the same time, or in sequence? At the same time, nothing good; calling it from one, having it await (block), then passing the coroutine to another thread and calling it there, you should be fine. But under the C++ proposal, I believe some of this is going to be determined by the implementation details of your coroutine. – Yakk - Adam Nevraumont Mar 30 '19 at 17:27
  • What is the difference if we rewrite the coroutine-based generator to something like `int generator() { static int i = 0; return i = (i + 1) % 100; }`? – L. F. Jun 23 '19 at 05:55
  • @l.f that generator's state is global? The coroutine's is not? – Yakk - Adam Nevraumont Jun 23 '19 at 17:29
  • 1
    @L.F. for such simple function maybe there is no difference. But the difference I see in general is that a coroutine remembers the entry/exit (execution) point in its body whereas a static function starts the execution from the beginning each time. The location of the "local" data is irrelevant I guess. – avp Sep 30 '19 at 10:51
  • So this runs both SIMD and threads but without developer caring about them? Good isn't it? – huseyin tugrul buyukisik Apr 07 '20 at 10:09
  • 1
    @huseyintugrulbuyukisik No. – Yakk - Adam Nevraumont Apr 07 '20 at 21:25
  • Thank you, this is extremely well written. Being more well-versed in C# and working with .net for far longer, I was able to draw several parallels from C# and better map the C++ variety into what I am already familiar with. – Tanveer Badar Aug 11 '20 at 15:44
  • Does C++20 have `std::expected`? I couldn't find it on cppreference.com – Aykhan Hagverdili Oct 06 '20 at 11:16
  • 1
    @AyxanHaqverdili No; latest proposal I can find is http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0323r7.html -- see https://www.reddit.com/r/cpp/comments/c75ipk/why_stdexpected_is_not_in_the_standard_yet_is_it/ for discussion about it – Yakk - Adam Nevraumont Oct 06 '20 at 13:07
23

A coroutine is like a C function which has multiple return statements and when called a 2nd time does not start execution at the begin of the function but at the first instruction after the previous executed return. This execution location is saved together with all automatic variables that would live on the stack in non coroutine functions.

A previous experimental coroutine implementation from Microsoft did use copied stacks so you could even return from deep nested functions. But this version was rejected by the C++ committee. You can get this implementation for example with the Boosts fiber library.

Lothar
  • 12,537
  • 6
  • 72
  • 121
0

coroutines are supposed to be (in C++) functions that are able to "wait" for some other routine to complete and to provide whatever is needed for the suspended, paused, waiting, routine to go on. the feature that is most interesting to C++ folks is that coroutines would ideally take no stack space...C# can already do something like this with await and yield but C++ might have to be rebuilt to get it in.

concurrency is heavily focused on the separation of concerns where a concern is a task that the program is supposed to complete. this separation of concerns may be accomplished by a number of means...usually be delegation of some sort. the idea of concurrency is that a number of processes could run independently (separation of concerns) and a 'listener' would direct whatever is produced by those separated concerns to wherever it is supposed to go. this is heavily dependent on some sort of asynchronous management. There are a number of approaches to concurrency including Aspect oriented programming and others. C# has the 'delegate' operator which works quite nicely.

parallelism sounds like concurrency and may be involved but is actually a physical construct involving many processors arranged in a more or less parallel fashion with software that is able to direct portions of code to different processors where it will be run and the results will be received back synchronously.

Dr t
  • 247
  • 4
  • 11
  • 12
    Concurrency and separation of concerns are *totally* unrelated. Coroutines aren't to provide information for the suspended routine, they *are* the resumable routines. – Ben Voigt Apr 19 '17 at 20:33