Hacker News new | ask | show | jobs
by secondcoming 1856 days ago
I'll admit I find coroutines difficult to grok. It seems to me that 'callback hell' is turning into 'coroutine hell'. The only plausible use-case I can see is enabling functionality similar to that of Python's `yield`.

Does threadpool::thread_loop() not have to check if the popped coroutine is suspended before attempting to resume it?

Are they really more efficient than normal callbacks when doing async?

3 comments

You're not the only one. Coroutines are complicated as hell and have too much boiler plate BUT once you handle it for a general enough case you get javascript-esque async await syntax which is very, very nice.

Take for instance, this code which relies on libuv for its event loop and co_await to retain its state during its execution: https://gist.github.com/Qix-/09532acd0f6c9a57c09bd9ce31b3023...

Lets say that you want to batch a bunch of database operations into one transaction. You could queue them up over the course of a few milliseconds, run the transaction, and then for each context that relied on different db operations simply return to each's previous point instead of having to call a handler. Granted, the handler is now inside of the `await_transform` needed to work with `co_await` but think of the possibilities. No weirdly separate callback function, no real need to make a class that encapsulates all of the operations for let's say a user's post request, and to top it all off, you can do this on a single thread. It's a tool for cleaner code but I'll be damned if it is really easy to understand.

It's just so much stupid boiler plate and a strange way of putting it together.

It still boggles the mind that they made it so hard to use this stuff in terms of boilerplate. Hopefully that will all be abstracted into a library that handles io, networking, multiprocessing and synchronization so most of us can just focus on writing the bits that do stuff. But I will never understand how the std maintainers could not manage to do this when every other language is doing async await support in their standard library Io utilities.
I think the reason it's like this is because the C++ maintainers are super focused on noticing whenever there is a choice to be made, and being careful not to make it for you. Given the option between foisting situationally bad performance on you and foisting lots of syntax and interface complexity on you, they always choose the latter. Which I suppose makes sense: C++ niche is that it's the Big Gun you pull out when there's serious work to be done. So they figure you're already going to be dealing with massive complexity anyway, and you're going to be prepared for it. May as well let give you _all_ the access rather than fumbling at the 1 yard line by making some opinionated choice that is a non-starter for somebody, somewhere.
> I think the reason it's like this is because the C++ maintainers are super focused on noticing whenever there is a choice to be made, and being careful not to make it for you

If you think that's the case, explain why I can't decide I want to pass an argument to a function in a register:

https://stackoverflow.com/q/58339165/1593077

Also, modern C++ has actually made it possible to write much more elegant code, for many delicate tasks, without sacrificing the performance benefits. So a convenient default doesn't necessarily have to contradict non-opinionated nature.

Most of that boilerplate will be hidden in the libraries which use it. Unless you are a library author I don’t know why you would care about the boilerplate-like elements from the standard— you will only use co_yield and co_await.
C++ compilers are famous for their cryptic error messages. Standard Libraries are the reason.
Lack of concepts was the reason, it will only get better now.
So far as I can see in 2026 the C++ programmers will be assuring us that (some feature from C++26) is going to fix all the awful clag in the C++23 programs which, in turn, offered (some feature from C++23, maybe it's a simpler exception mechanism) but alas instead created more clag, despite their promise to clear up the mess from C++20...

I am an old man, and so I remember when left and right every C++ programmer was excited about how the Standard Template Library was going to make everything OK and those of us who were still jeering would be writing C++ soon. How did that go?

One reason could be that C++ tends to be used more in performance-critical contexts, and frequently the abstractions present in "every other language" can get in the way of performance.
also the memory management model in c++ is much more complex - most other languages just use garbage collection to deal with reference lifetimes
garbage collection is one of the abstractions that can interfere with performance. it's out of the question for real time contexts, for example.
On Windows that library is WinRT.
There are a lot more possibilities. The reason why they are so complicated is simply to give the library developers all possible options. This features is afaik something targeted at them, so that boost, asio and others can implement powerful solutions without language restrictions.

The only way to enqueue a coroutine is to call schedule() within a co_await statement/expression. In this process the coroutine is suspended. Therefore there should not be any coroutine within the queue, which we cannot immediately resume.

I'm afraid I don't have any numbers available to compare coroutines with other approaches. But nevertheless in my opinion coroutines are benefitial because they keep their state (the stack frame, local variables) alive. If you use callbacks you would have to handle all these things yourself. Think about a generator for a sequence of numbers. You would have to store at least the counter variable manually. With a coroutine this happens automatically.

For what it's worth Python's yield pretty much is a coroutine, as it has a (very rarely used) feature that allows information from the caller to be passed back to the generator.

The c++ implementation seems closer to lisps 'call with current continuation', though as far as I can tell all implementations achieve more or less the same thing (though thread safety might vary among the options).

Actually continuation passing style (callbacks) are another way of doing the same thing, though they have the disadvantage that they require large structural changes to the code. It wouldn't surprise me if the callback hell can therefore also occur in all versions, though some might make it easier than others (python's implementation in particular makes it somewhat less likely by encouraging information to flow one way)

> as it has a (very rarely used) feature that allows information from the caller to be passed back to the generator.

The Twisted library encouraged heavy use of this before Python implemented async/await.

https://twistedmatrix.com/documents/current/core/howto/defer...