Live data from Hacker News

The Downsides of C++ Coroutines

reductor.dev

41–50 of 51 posts

Re: The Downsides of C++ Coroutines

#41

A lot of these seem to be downsides of manual memory managment in C++, not stackless coroutines. The same kind of coroutines work fine in Python, Javascript and C#. Some 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 abl…

They don't work just fine in C#, there is a reason why one of ASP.NET architects has written a guide of best practices.

https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/b...

Ironically, C++'s design is heavily related to C#, as the initial proposal was done by Microsoft and shares many of the same ideas, including how to create runtime aware awaitable types.

Re: The Downsides of C++ Coroutines

#42

Earlier quoted context omitted.

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…

Having seen a number of footguns with references and lambda captures as well where the compiler won't warn/catch I don't think it's unique to coroutines. 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…

That is why no sane person should use C or C++ without static analysis, at very least on the CI/CD pipeline, lint wasn't created in 1979, only because Stephen Johnson was bored at Bell Labs.

Re: The Downsides of C++ Coroutines

#43
One problem I have with C++ coroutines is the implicit capture of the "this" pointer in member functions. One can also accidentally pass arguments by reference resulting in lifetime issues, but at least the parameter types are explicitly stated.

The post mentions that it might be the caller's or the callee's responsibility to keep the object alive until the end of the coroutine. This is purely based on conventions however, and different libraries might have different conventions. If the caller is responsible for it, extra care needs to be taken whenever the function is called -- the code shown in the post seems fairly complex to me and easy to get wrong. Also, I am not sure how the callee could safely implement keeping the object alive if lazy coroutines are used: Even if the first statement in the coroutine is retaining a strong reference on the object, there might still be a time between the call and the initial resume of the coroutine where the object is destroyed. I think it would have been great to provide explicit capture lists for coroutines, similar to lambdas.

All of this gets especially confusing once you try to use lambdas together with coroutines. AFAIK, C++ lambdas are basically just structs overloading operator(). In a coroutine, only the "this" pointer of the structure is captured and the caller needs to ensure that the object is not only alive, but also at the same memory address until the end of the coroutine. This is very easy to get wrong in my experience.

Re: The Downsides of C++ Coroutines

#44

A lot of these seem to be downsides of manual memory managment in C++, not stackless coroutines. The same kind of coroutines work fine in Python, Javascript and C#. Some 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 abl…

Isn't this wrong (the "green threads are just threads" part)? The green thread / stack switching implementations I've seen so far all used cooperative multitasking, eg you know exactly where control is handed back to the scheduler and don't need synchronization between green threads - assuming the scheduler keeps all green threads on the same OS thread of course)

Blocking vs non-blocking can be solved with naming conventions, like Sync vs Async suffix (works well in node.js for instance)

(also getting rid of colored functions is a good thing!)

Re: The Downsides of C++ Coroutines

#45

A lot of these seem to be downsides of manual memory managment in C++, not stackless coroutines. The same kind of coroutines work fine in Python, Javascript and C#. Some 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 abl…

Isn't this wrong (the "green threads are just threads" part)? The green thread / stack switching implementations I've seen so far all used cooperative multitasking, eg you know exactly where control is handed back to the scheduler and don't need synchronization between green threads - assuming the scheduler keeps all green threads on the same OS thread of course) Blocking vs non-blocking can be solved with naming con…

You have to be careful. Stackless coroutines will only schedule if you call co_await, but a stackful coroutine can be scheduled in the depths of any call.

This can trick 2 parts of your program into thinking they have exclusive access to the same thing at the same time. E.g. they could both grab the same thread local or both enter the critical section of a recursive mutex.

Re: The Downsides of C++ Coroutines

#46

Coroutines 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…

C++ solved this by shipping something that are not coroutines and branding it coroutines anyway. … They aren't coroutines, and by squatting on the name, make it borderline impossible that C++ will ever have coroutines. 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.…

C++ had two competing designs for "coroutines". One is syntax sugar over a control flow graph rewrite with implicit state for keeping track of where to branch to. The other is syntax sugar over swapping stacks of execution.

The version that shipped can be done in the compiler front end. On the happy path it compiles to zero cost relative to writing the branches by hand. Machine architecture independent.

The version that didn't ship requires language runtime support. It involves allocating memory for the new stack and storing the live registers to it on yield. It's per-platform machine code, with varying overhead depending on how much control the compiler gives over calling conventions. Yield then looks a lot like a function call (and sometimes upsets branch predictors).

The full/stackful/green/thick/etc version works very like a posix thread without the pre-emptive scheduler, and needs language runtime support for exactly the same reasons that pthread_create does. They're zero cost if not used - they don't change the calling convention of other functions - but the yield usually can't be optimised out at compile time if they are used.

Naming things is indeed difficult and definitions do tend to shift over time. However the "C++ has coroutines now" feature box check doesn't bear up under scrutiny if one expects said coroutine to support the same operations that coroutines support in other languages.

Re: The Downsides of C++ Coroutines

#47

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…

Yeah, though that’s also kind of my point. You know and I know (having learned the hard way) that the mechanism involves storing parameters and intermediate values in an object that is referred to later. It’s obvious now I know, but the design hides that from the user - they aren’t supposed to care about that. However, the footguns are still present. There should have been language features to prevent temporaries binding to const& for coroutines, but (according to a friend) the language doesn’t distinguish coroutines and subroutines at that level (… or something?)

Same problem with ranges. The footguns are remnants of abstracting something complex with a friendly interface and failing to secure it. It’s great for people who know the implementation, and it’s obvious where memory issues appear - but if you don’t know the implementation then you end up with holes in your feet.

Re: The Downsides of C++ Coroutines

#48

Earlier quoted context omitted.

C++ solved this by shipping something that are not coroutines and branding it coroutines anyway. … They aren't coroutines, and by squatting on the name, make it borderline impossible that C++ will ever have coroutines. 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.…

C++ had two competing designs for "coroutines". One is syntax sugar over a control flow graph rewrite with implicit state for keeping track of where to branch to. The other is syntax sugar over swapping stacks of execution. The version that shipped can be done in the compiler front end. On the happy path it compiles to zero cost relative to writing the branches by hand. Machine architecture independent. The version t…

Thanks for laying out the C++ situation. Your concern makes practical sense.

Re: The Downsides of C++ Coroutines

#49
post #41

A lot of these seem to be downsides of manual memory managment in C++, not stackless coroutines. The same kind of coroutines work fine in Python, Javascript and C#. Some 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 abl…

They don't work just fine in C#, there is a reason why one of ASP.NET architects has written a guide of best practices. https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/b... Ironically, C++'s design is heavily related to C#, as the initial proposal was done by Microsoft and shares many of the same ideas, including how to create runtime aware awaitable types.

I've never used ASP.NET, I mean they work fine in GUI apps where you click a button, start downloading a file, and then show a message when the file has been downloaded.

Making code nonblocking without threads and without callbacks = happy case for async. Writing multithreaded servers focussed on throughput is a whole other can of worms, which is basically my point.

Re: The Downsides of C++ Coroutines

#50

A lot of these seem to be downsides of manual memory managment in C++, not stackless coroutines. The same kind of coroutines work fine in Python, Javascript and C#. Some 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 abl…

Isn't this wrong (the "green threads are just threads" part)? The green thread / stack switching implementations I've seen so far all used cooperative multitasking, eg you know exactly where control is handed back to the scheduler and don't need synchronization between green threads - assuming the scheduler keeps all green threads on the same OS thread of course) Blocking vs non-blocking can be solved with naming con…

I don't know, I've always thought green threads refers to the coding style of Go or Java's Project Loom - you write code that looks like multithreading and call blocking methods like `socket.receive()`. And then deep down in each IO call, there is some magic that suspends the green thread, and resumes it when data is available.

I think the colored function thing is often thoroughly misunderstood. There is a real difference between a function that returns `string` vs. `Future`. It's not arbitrary but just a matter of typing. Languages could have more syntactic sugar to bridge both worlds of course. And you can get rid of the distinction as goroutines etc. show.

But actually I wonder if it would be useful to keep some colors. Maybe you could have an effect system and mark functions as "computationally expensive" / "blocking" vs. "computationally trivial". The compiler would prevent you from calling the blocking functions from the GUI thread, but you could `async` or `go` them to another thread and resume when finished.

Post reply on HN