Earlier quoted context omitted.
It is due a bit of proof reading. There are some readability issues. That being said, it is a great article. C++ and coroutines is a story that has been going on for a long time, and the result surprised me. In a bad way. One bit me right from the start. I copied out an example and it crashed, and it turned out (after hours of searching, reading - the compiler and sanitisers sure weren’t any help) that the problem wa…
The compiler won't stop you storing a pointer to a local variable and dereferencing it later, either. That's just the nature of programming without managed memory. Calling a coroutine is essentially the same as temporarily "returning" from the current stack frame, so all of the usual practices around taking pointers apply. I agree with your conclusion of not using C++ coroutines, though. It seems like the design fall…
The Downsides of C++ Coroutines
31–40 of 51 posts
Re: The Downsides of C++ Coroutines
#32Earlier quoted context omitted.
Two differences: First, stackful coroutines use the coroutine stack for everything they do. Stackless coroutines can use the normal thread stack for synchronous calls, and that stack can be shared across any number of coroutines. Per-coroutine allocation is only needed for asynchronous calls. Second, for stackful coroutines you need to allocate the entire stack up front, and usually you have no way of knowing how muc…
Technically you could manually grow coroutine stacks the same way the kernel does for thread stacks, by mapping on fault and periodically unmapping everything beyond the red zone. But the complexity would be significant and hard to make it efficient without kernel support.
Re: The Downsides of C++ Coroutines
#33Earlier quoted context omitted.
It is due a bit of proof reading. There are some readability issues. That being said, it is a great article. C++ and coroutines is a story that has been going on for a long time, and the result surprised me. In a bad way. One bit me right from the start. I copied out an example and it crashed, and it turned out (after hours of searching, reading - the compiler and sanitisers sure weren’t any help) that the problem wa…
The compiler won't stop you storing a pointer to a local variable and dereferencing it later, either. That's just the nature of programming without managed memory. Calling a coroutine is essentially the same as temporarily "returning" from the current stack frame, so all of the usual practices around taking pointers apply. I agree with your conclusion of not using C++ coroutines, though. It seems like the design fall…
We were using coroutines about ~12 years ago in embedded contexts, this was with Lua which has very good support and being a managed language avoids all the footguns here while still allowing very fast interop with native code(at least in the case of LuaJIT).
I hate to drag Rust into every C/C++ conversation but this is one area where the language really keeps you within the guardrails. Callbacks are hard to use correctly in Rust, less because of the language and more just do to the messy lifecycle aspect of them. You can side-step that with shared_ptr/Arc but then you're stepping into memory leak territory when you have a circular reference(and the atomic ref counting isn't cheap either).
Re: The Downsides of C++ Coroutines
#34Re: The Downsides of C++ Coroutines
#35This article hits on a number of interesting points. There is a lot of complexity to be aware of when using C++ coroutines. And a number of “normal” practices become dangerous in them, such as pass by reference. That said, I think they are still very much worth it. Older asynchronous programming libraries in C++ are so verbose and so much worse than coroutines that it’s an obvious choice to use coroutines. Also, ther…
>RAII lock wrappers You don't even need coroutines for this to be dangerous. Holding locks over callback invocations is a pet peeve of mine in PR reviews. Callback invocations, like suspension points, can inject arbitrary operations into our code, which can easily break prior invariants, yet look innocuous for the casual reader.
Re: The Downsides of C++ Coroutines
#36A coroutine is a thread of execution that can yield to another. The scheduler is thus under userspace control.
The C++ thing is syntax sugar over a control flow transform which looks kind of similar, except you have to annotate all the functions in the call tree and can't do anything that can't be desugared to the same runtime system that was there anyway.
The primary downside of C++ coroutines is then clear. They aren't coroutines, and by squatting on the name, make it borderline impossible that C++ will ever have coroutines. This is annoying as they're one of the things which really needs compiler support to do well.
Re: The Downsides of C++ Coroutines
#37Re: The Downsides of C++ Coroutines
#38Earlier quoted context omitted.
Technically you could manually grow coroutine stacks the same way the kernel does for thread stacks, by mapping on fault and periodically unmapping everything beyond the red zone. But the complexity would be significant and hard to make it efficient without kernel support.
For a while there was an exciting patch for gcc called split stacks that provided a little thunk for every function -- one normally bypassed, but which stackless coroutines could opt in to call -- that would check if more stack had to be allocated, but I think the story was that Go was the primary potential customer for it and they decided to just give up on the dream :(.
Re: The Downsides of C++ Coroutines
#39Some parts like the one about lazy coroutines seem to only be an issue because coroutines in C++ can theoretically resume on any thread. If they were restricted to the current thread by default (like in Python+Twisted I think?) then you would still be able to use them for many use cases but with less cognitive overhead.
The author seems to prefer stackful coroutines aka green threads, which are essentially just user mode threads. Handoff is implicit deep inside 'blocking' functions. I rarely see the downsides of them discussed, but they have their own problems: They are still threads so you often need locks. You don't know where a function will hand off, and you could accidentially call a really blocking function or run a long computation and ruin responsiveness. And the type of the function no longer reflects if it is blocking or not (the famous colored functions).
Re: The Downsides of C++ Coroutines
#40Coroutines have a performance cost associated with allocating a stack. C++ solved this by shipping something that are not coroutines and branding it coroutines anyway. Sometimes it still needs heap allocation, but of a constant size and smaller thing. A coroutine is a thread of execution that can yield to another. The scheduler is thus under userspace control. The C++ thing is syntax sugar over a control flow transfo…
I am not a C++ person, but I protest this characterization. You obviously know the categories and I suppose you know the history, but I will recount them so that everyone else understands my objection.
O.G. coroutines emerged in a world where subroutines were, by and large, not reëntrant. Function parameters, local variables, and return addresses were static, and there was no call stack in the modern sense. This is why the old timers said that coroutines were a generalization of subroutines; only the jump instruction of the call/return really needed to change.
Once call stacks became common, coroutines fit awkwardly, and people tried to adapt them in various ways.¹ The world has settled on two designs for reconciling coroutines to the call stack: thick and thin coroutines. Thin coroutines allow suspension within the body of the coroutine but not within subroutine calls; this way the size of the coroutine’s state can be known at compile time and its interaction with the stack is relatively clear. Thick coroutines (i.e., green threads) can be suspended within a subroutine call, and thus require their own slices of stack — either separate from the main stack or copied from and to it as the coroutine is suspended and resumed.
Thin coroutines are absolutely coroutines. They are truer to the original definition than thick coroutines are! They are more limited than thick coroutines, true; whether that makes them better or worse is a matter of design trade-offs. But they certainly deserve the name.
[1] Simula 67, to my understanding, treated objects as a kind of coroutine instance where function definitions in the coroutine body became methods that closed over its local variables.