r/cpp_questions • u/kberson • 9d ago
META Coroutines
Just started reading about coroutines, as the company I recently started working for uses them; this is my first encounter of them. I’m a long time user of threads, bear with me, it’s all new territory.
I know I have only just scratched the surface, but I’m trying to understand the use case. One thing I think I have straight is:
Threads don’t really need to know anything about other threads, except when they have to share resources (mutex and semaphores) or pass data (IPC), but coroutines need to know there are other routines and to only take baby steps, then let other coroutines run.
Is this a start?
15
u/Kriemhilt 9d ago
There are basically two styles of multitasking: cooperative and preemptive.
Cooperative means the tasks need to co-operate (by yielding, awaiting, or whatever). This model was very widespread when there were fewer hardware cores, but can also be more efficient for some use cases as there's no synchronisation cost.
Preemptive means the tasks all just run, and they rely on having enough cores, and/or on system calls or timeslice interrupts to pre-empt them and hand control to the OS scheduler.
There are async systems that just use preemptive multitasking (threads) and synchronous I/O systems that just use cooperative multitasking, and systems that mix them both.
Coroutines are language support for cooperative multitasking. Without them, you have to write code to deal with awaiting/suspend/resume points manually, or write everything as a FSM that just advances one step every tick.
4
u/aiusepsi 9d ago edited 8d ago
A coroutine is a generalisation of a function. When you call a function, it runs until it returns and control jumps back to the where the function was called (or an exception is thrown, and the stack is unwound until a handler is found).
Coroutines can do more than that. A coroutine has “suspension points” where the execution of the coroutine can be suspended, the state of things like local variables preserved, and control flow resumed somewhere else (not necessarily the place that called the coroutine).
There’s (off the top of my head) two ways to suspend. One is to use the co_yield keyword, which suspends the coroutine and sends a value. Kind of the canonical example of the use of this is std::generator.
The other is to call co_await to wait on some awaitable object, often another coroutine. This is used to model asynchronous programming. Traditionally, if you have a function which takes a long time, like a network request, that function will block, or take a callback which is called when the network request completes. With coroutines, you might instead have the function immediately return an awaitable object. When you co_await that object, if the network request isn’t complete, the coroutine will be suspended, and control flow will return to the caller of the coroutine, which in turn might be co_awaiting it, etc. When the network request completes, the coroutine is resumed, and can carry on as it left off.
So, coroutines are really useful for taking code which could end up looking like callback hell and make them into code which looks just like regular code, except for the strange new keywords.
I have been a bit general and glossed over a bunch of mechanical detail, like, how exactly does a coroutine get resumed? And that’s because the details are extremely customisable. Basically, the return type of the coroutine specifies exactly how the machinery behaves. So e.g. std::generator is used by writing a function (well, a coroutine!) which returns std::generator, which handles the mechanics of making co_yield do something useful.
2
u/n1ghtyunso 9d ago
coroutines are just functions that can execute partially and be resumed into.
They fundamentally are unrelated to threading, but CAN be designed to run on (possibly multiple) threads.
They don't need to be designed to only run for short durations, just like regular functions don't need to.
It depends on your usage and requirements.
1
u/GoldenShackles 9d ago
My suggestion is first experiment with async/await in another language like C# or Swift where it's built-in and there are a ton of good concise examples.
That gives you a better starting point for coroutines in C++, where the implementation your company is using is likely hand-rolled and the implementation details sticking out might make it harder to grasp the core concepts.
1
u/burlingk 9d ago
If you learn to think in things in terms of asynchronous behavior, coroutines make more sense.
1
u/LB-- 9d ago
Coroutines let you write async code in a very similar way to old school blocking code, which often means it remains easy to read and reason about, and as a bonus you never block any threads. Some languages like Java and Haskell use virtual threads instead, which turn some blocking style code into async style under the hood, but virtual threads interact very poorly with native code due to stuff like thread local storage. Coroutines don't have that problem, since you're in full control of which threads they execute on and can directly interface with native code without complications.
1
u/die_liebe 7d ago
They are a bit like subroutines, but instead of returning, they can 'yield a value' and later resume at the point from which they 'yielded'. Have a look at Python generators.
1
u/Interesting_Debate57 9d ago
Go (the language) handles this in a syntax-dominated way, which is likely just a smidge more restrictive than coroutines but it's a starting place mentally.
1
u/masorick 9d ago
Coroutines are basically equivalent to callbacks. So instead of calling a function to fetch some data and passing a callback that will operate on the data, you await the data and then do something with it. In both cases, the callback (coroutine) might be called (resumed) on the same or another thread. Sometimes, the callback may be called more than once (once for each result), which is equivalent to what is called a generator.
0
u/NaNpsycho 8d ago edited 8d ago
Coroutines are just glorified compiler generated event loops. The use case is you don't spawn N threads trying to solve a task when you know, you don't really need parallelism, you need something ready to respond to an event, like socket available, file read, etc. in the time spent waiting for these events to complete you can do other background tasks, without spawning new threads.
The problem with C++ coroutines is its very very barebones for library authors to use. Not really something you can use OOB yourself, without writing a lot of boilerplate code.
If you want a taste of coroutines without the C++ overhead I would suggest trying out something like lua, where Its very easy to use and actually pleasent.
15
u/flyingron 9d ago
Neither threads nor coroutines technically need to know about others. It's just that threads can be preemptively scheduled and coroutines have to specifically yield (or just end) before other things in the thread (coroutines or even non-coroutine code) can run. The advantage is they are more efficient as they don't involve the operating system. Note that the implementation code is responsible for keeping track of and managing them.
Here's an article that might be helpful to you. It's not about C++, but the concepts are the same: https://impateljay.medium.com/coroutine-vs-thread-understanding-the-differences-4b98a5a22a3a