Do you use coroutines?

I'm curious about people's experience with coroutines. I don't think I've seen them get more than a mention on this forum, much less any sample code.

Do you guys use them at all? If yes, what do you think? If no, why don't you?

My impression is that most people ignore them because
1. There's no support library;
2. They're too "weird" or "unfamiliar", and
3. Something's lacking in existing tutorials.
Opinions?
Yes, I use coroutines for implementing a signal driven cooperative multitasking mainly within a gui environment. The advantage is that tasks do not interfere (i.e. no protection necessary) but still appears to be parallel. For instance showing a clock while waiting for user input.

1. There's no support library;
There's at least boost:

https://www.boost.org/doc/libs/1_82_0/libs/coroutine2/doc/html/index.html

2. They're too "weird" or "unfamiliar"
It is indeed a bit hard to grasp...

3. Something's lacking in existing tutorials.
Guess it is a bit hard to understand the advantages over normal/recursive function call even with tutorials. For instance this:

https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
After reading the link in 3 (thanks). I've now got this terrible pain in all the diodes down my left side....
There are two use-cases where I regularly use coroutines:

Writing generators to "lazily" create a sequence of elements, i.e. only generate each element of the sequence when it's actually needed. And, most important, GUI programming. Actually, I'm using async functions in C# (which essentially are coroutines) a whole lot! This way you can write "non-blocking" functions, i.e. functions that run directly in the GUI thread but that still won't "freeze" your GUI while waiting for I/O operations (or the like) to complete. That is so much more elegant and less error-prone than having to kick off a separate thread for each "long-running", e.g. I/O, operation – and then having to explicitly synchronize with the GUI thread in order to "show" the result 🙄

As far as C++ is concerned, I think the coroutines support added in C++20 is more a "low level" feature, which is mostly intended for library developers. Even if you look at "minimal" C++ coroutine examples, they require an amazing amount of boilerplate code, because of all the return types and promise types that need to be defined! That's probably the reason why C++ coroutines are not used so much yet...

std::generator from C++23 seems like a big step forward:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <generator>
#include <ranges>
#include <iostream>
 
std::generator<char> letters(char first)
{
    for (;; co_yield first++);
}
 
int main()
{
    for (const char ch : letters('a') | std::views::take(26))
        std::cout << ch << ' ';
    std::cout << '\n';
}
Last edited on
I'm a big fan of coroutines, but I have as many anecdotes of removing them from projects as I have of using them in projects. I think to be able to use them not just as simple generators, but as basic execution primitives like threads are, you need a lot more support from an implementation than merely being able to start, yield, and resume coroutines. At a minimum you need an execution manager than can take care of scheduling them to run in threads, and synchronization primitives that let you make a coroutine wait for an event (without blocking the thread it's running on).
'yield' in C# (possibly in combination with await) is an example of a coroutine, right? At least, basic use.
If so, that's the only time I've used coroutines.

I'll read that Stanford link when I get the time.
Last edited on
IEnumerable-returning functions are stackless coroutines. You can only yield from the first function:
1
2
3
4
IEnumerable<int> foo(){
    for (int i = 0; i < 10; i++)
        yield return i;
}
Stackful coroutines are more powerful. They're able to preserve the entire context, so any function is able to yield the coroutine, no matter how deep in the stack. For example, you could use a stackful coroutine to write a recursive traversal function that appears at the point of usage to flatten a tree.
1
2
3
4
5
6
7
8
9
10
void traverse(Node n){
    yield(n);
    for child in n.children{
        traverse(child);
    }
}

for node in traverse(root){
    //...
}
'yield' in C# (possibly in combination with await) is an example of a coroutine, right? At least, basic use.

In C#, yield is used with generators (semi-coroutines) and has existed since version 2.0.

await is only allowed inside async functions, which were added in version 5.0. I think async functions are "fully-fledged" coroutines.
Last edited on
I've worked for many years with stackless coroutines, in a multi-MLoC project.. in a different programming language.
Took a little bit of getting used to code discipline and a LOT of getting used to (and writing our own) debugging and profiling.

Consider what a stack trace from coroutine looks like: default call stack just step to scheduler that's running this section of the coroutine, but what we need are coroutine(s) awaiting our result, and coroutine(s) that caused ours to be scheduled in the first place.

in my current C++ codebase we have execution graphs on top of TBB scheduler and an experimental coroutine branch that nobody has the time to productionize. Maybe next new hire.
Topic archived. No new replies allowed.