Live data from Hacker News

My tutorial and take on C++20 coroutines

scs.stanford.edu

111–120 of 144 posts

Re: My tutorial and take on C++20 coroutines

#111
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

There's none in the standard library, but e.g. Seastar already has coroutine support implemented for its futures and it works really well - the code looks clearer and in many cases it reduces the number of allocations to 1 (for the whole coroutine frame).

Re: My tutorial and take on C++20 coroutines

#113
post #23

Its insane how complex C++20 coroutines are when compared with Rust coroutines.

Can you say more on this? What is Rust doing differently here that simplifies things?

I find the Rust design very simple: a coroutine is just a state machine, i.e. just a C struct. I find this very easy to reason about.

It does not require memory allocations, does not require a run-time, works on embedded targets, etc.

Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. which is the cherry on top.

Re: My tutorial and take on C++20 coroutines

#114

Earlier quoted context omitted.

Also, if it’s not clear, within a coroutine, you can call any function (or coroutine) you want. It’s just that to co_yield, you have to be in the coroutine not deep in the stack.

Isn't it like that is most languages? I'm thinking about Python, C#, JS. If you call a blocking function from an async function, you cannot yield from deep inside the blocking function. Why is this a big deal in C++? Am I missing anything that makes c++ coroutines less powerful than other mainstream solutions? Or are people comparing its power with e.g. Lisp or go?

It's a big deal because, while it has some downsides, being stalkless means they can have next to no overhead, meaning it can be performant to use coroutines to write asynchronous code for even very fast operations. The example given https://www.youtube.com/watch?v=j9tlJAqMV7U&t=13m30s is that you can launch multiple coroutines to issue prefetch instructions and process the fetched data, so you can have clean code that issues multiple prefetches and process the results. Whereas in Python (don't get me wrong, I love Python) you might use a generator to "asynchronize" slow operations like requesting and processing data from remote servers, C++ coroutines can be fast enough to asynchronously process "slow" operations like requesting data from main memory.

Re: My tutorial and take on C++20 coroutines

#115
post #108
post #18

Part of this is that I’m tired, but it blows my mind how difficult C++ coroutines are as someone who considers themselves decent at C++ (although maybe I’m not) and uses coroutines in other languages. The amount of code needed to do almost nothing is extraordinary, and putting it all together doesn’t seem like you would often get on your first try. I get that new keywords basically can’t be added, but man, that’s pai…

This is how I view iterators, but with an extra notch in difficulty. You really need to understand the coroutine model to get anywhere -- and the same is true of iterators. Are these leaky abstractions? I generally think of leaks as "pushing", but this is "pulling" -- I'd call 'em sucky abstractions. But I digress. I just sat down with the tutorial and banged out what I consider to be a holy grail for quality of life…

So, given sibling comments on stack and function calls, this:

    skip_iter(int skip, int stop)
      for(int i=0;i
Wouldn't work with complex types, boxed integers and so on? Because calling postfix ++ would be a function/method call?

Re: My tutorial and take on C++20 coroutines

#116
post #115
post #108

Earlier quoted context omitted.

This is how I view iterators, but with an extra notch in difficulty. You really need to understand the coroutine model to get anywhere -- and the same is true of iterators. Are these leaky abstractions? I generally think of leaks as "pushing", but this is "pulling" -- I'd call 'em sucky abstractions. But I digress. I just sat down with the tutorial and banged out what I consider to be a holy grail for quality of life…

So, given sibling comments on stack and function calls, this: skip_iter(int skip, int stop) for(int i=0;i Wouldn't work with complex types, boxed integers and so on? Because calling postfix ++ would be a function/method call?

That's just a simple example. I think the following should work, possibly with modification for forwarding:

    class Stooge {...};

    CoroutineIterator three_stooges() {
       co_yield Stooge("Larry");
       co_yield Stooge("Curly");
       co_yield Stooge("Moe");
    }

Re: My tutorial and take on C++20 coroutines

#117
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

Are C++20 coroutines allowed to be recursive? Or does recursing require boxing?

For a stackless coroutine the compiler normally has to build a state machine to represent the possible execution contexts, but if the state machine includes itself then it has indeterminate size. Normally you solve this by boxing the state machine at yield points and using dynamic dispatch to call or resume a pending coroutine - which may be significantly slower than using a stackful coroutine to begin with (in which case, the stack is allocated on the heap up front, leading to a higher price to spawn the coroutine, but lower to resume or yield).

Re: My tutorial and take on C++20 coroutines

#118

Seems like once again we are stuck with an overly complex and un-ergonomic design that is going to burden every single C++ programmer for a decade, until someone gets fed up and fixes it. Just like chrono, just like random, just like std::string, all of std::algorithm, etc. God damn it.

You have hope that someone is going to fix it? You're an optimistic soul :)

Re: My tutorial and take on C++20 coroutines

#119
Keep in mind that these is a really basic building block where you can bring your own runtime and hook coroutines into it, not something that is at all usable out of the box. This is exacerbated by the fact that the C++ standard library is still lacking support for non-blocking futures/promises and queued thread pools to run them.

To see how it can be used for actual asynchronous operations on a thread pool, take a look at asyncly, which I co-authored:

https://github.com/LogMeIn/asyncly/blob/master/Test/Unit/fut...

Re: My tutorial and take on C++20 coroutines

#120

Earlier quoted context omitted.

Can you say more on this? What is Rust doing differently here that simplifies things?

I find the Rust design very simple: a coroutine is just a state machine, i.e. just a C struct. I find this very easy to reason about. It does not require memory allocations, does not require a run-time, works on embedded targets, etc. Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. w…

Technically it's an enum, or tagged enum.
Post reply on HN