So I became fascinated by C++ coroutines late last week, and have spent a good portion of the weekend figuring out how they work.
The TL;DR of my discoveries so far is that C++20 coroutines:
1) follow the best and the worst of C++ practices, and
2) are a really clever way to decouple stackless functions from their execution model.
For (1), well, it's C++. No crisp & clean syntax, lots of weird compilation errors, and even weirder runtime errors. I hated them with passion for the first several hours. But then they grew on me, because the flip side is also true. The flip side is backwards compatibility and "only pay for what you use". And introducing a totally new concept — the stackless execution model, effectively, [proto-] green threads — into a mature and complex language is one hell of a pain!
For (2), I do believe it's a genius solution for the language to separate stackless functions from their execution model. And C++20 did exactly that.
There is literally no event loop in C++20! The claim (as I understand it) is that a tiny microcontroller may want to use C++20 coroutines, and the authors of the Standard are very explicit about not constraining possible runtimes.
All that C++20 coroutines do is introduce stackless functions into the language as first-class citizens. And yes, stackless immediately implies resumable.
So it is one step about the most-clever-possible syntactic sugar over `setjmp()`. Full one step.
But not a millimeter more than this one step.
Whether you "execute" your coroutines in a dedicated single-threaded event loop, or a thread-pool-based event loop, or by individual steps via some timer event or even a network packet — that is entirely your call.
The stackless functions are there, `co_await` (as well as `co_yield` and `co_return`) is there, no callback hell is needed, and all the RAII and scoped initialization/ownership model is fully intact.
Now, I'm still elbows deep in my experiments, but here's the first big LinkedIn-worthy question. Why don't we write our C++20 [unit] tests as corourines?
Say, you want to limit some test to five seconds. And fail it as "timed out" within these five seconds.
Before coroutines, doing this without instrumenting the very code of the test involved `fork()`-ing the process, effectively relying on the operating system's primitives to ensure proper resource deallocation.
With C++20 coroutines, as long as the slow test is slow on non-blocking I/O (i.e. not in an infinite loop, but on something that `co_await`-s stuff), it is possible to time-box each individual test to some five seconds very much organically.
Because a coroutine frame, unlike a stack frame, can be `.destroy()`-ed safely and cleanly.
So, are we doing it yet? If no, why not? If yes, what would be some next-gen `gtest` to look into?
Also, I'll be writing some code and blog posts on C++20 coroutines soon. If you are reading this text and are an expert, DM me pls — I'd love to pick your brain!
19
4 Comments