C++ Coroutines
nigeltao.github.io
nigeltao.github.io
This is a persistent misunderstanding about async... it is not generally more efficient than non-async code. In fact, the overhead of async may slow things down. (There can be other downsides as well. E.g. debugging may be more difficult.)
For it to help, you need to have something better for the current thread to be doing than waiting for whatever it is you're waiting for.
Whatever that is, another worker thread could be doing it, so what we're really talking about is that async may let you reduce the number of threads you're using.
(Async is so useful in JS because it lets you reduce the number of threads to one, which lets you sidestep all kinds of hairy issues.)
That might be good enough, depending on your situation, to overcome the downsides of async worthwhile. But it may very well not.
Anyway: don't use async because it's "more efficient".
Now, io_uring async code is yet another story.
I was working at LimeWire when Apple finally shipped a 1.4 JVM as their system JVM. The whole thing got much lighter weight when we could use non-blocking I/O.
That is, unless you're using io_uring, POSIX AIO, etc., in which case your async code might not have the extra layer of abstraction on top of the system APIs.
One point also is that until recently most personal computers did not have multiple cores, aka you could think at the lowest level that even multiple threads were just more dangerous async lol
Even making new threads in the JVM isn’t guaranteed to result in more than a single OS thread.
core 2 duo has been released 17 years ago
(Yeah, that's still a decade ago, but that's a good deal more recent than 17 years.)
Doing more work in a single thread saves a lot of context switches, especially if IO makes up a large portion of the workload. And even more so with other mechanisms to minimize syscalls, like io_uring on Linux.
Async overhead is highly dependent on the way the async runtime is implemented.
It's true that async requires scheduling and task switching to be implemented in userspace, but assuming that async is used as an alternative to threads, then the OS would be doing a similar amount of work anyway.
The OS may be able to do that more efficiently because it has more information about the system. Or it may be the opposite, because the application has more information about the workloads.
"async = overhead" as a general statement is not correct.
And kernel mode actually has much more efficient and performant context switches, because the kernel can access hardware directly.
(It is sometimes useful to be able to do custom user mode scheduling, but certainly not because of "context switches".)
In my experience, the exact opposite is true, particularly in the era of CPU mitigations that require TLB flushes upon every kernel-mode context switch.
User-level can also have the advantage of having more actual context about the task that is running, meaning that it's often able to avoid saving/restoring as much data as a kernel-level switch would. See Go's green threads for a great example of this kind of cooperation between runtime and language.
> Do you have a citation for kernel mode having more efficient context switches? What kind of direct hardware access are you referring to that would be better than pushing the register context onto the stack?
The closest thing to this that I can think of is on 32-bit x86 which did have hardware assisted context switching via TSRs.
As it happens, everybody stopped using it because it was too slow, and a bit painful unless you fully bought into x86's awful segmentation model. Early Linux kernels use it if you want to see it in action.
This is unrelated to kernel threading.
If you have 1 thread handling 1000 requests with some async io mechanism (epoll, io_uring, ...) ,instead of 1000 threads each handling one request, there are much fewer threads fighting over CPU cores and the 1 thread can stay active much longer, hence reducing the amount of context switches.
Especially with a mechanism like io_uring, which helps minimize syscalls (and hence switching to kernel threads).
(When the kernel decides which thread gets to run it's doing the equivalent of an epoll call.)
I don't think efficiency has only one meaning. For instance, you can create millions of coroutines while you're limited to only thousands of threads. Doesn't that mean your program is making better use of your hardware and thus is more efficient?
Threads usually preallocate some stack space, while coroutines usually don't. But that isn't such a big deal.
Pthreads work differently, but historically kernel-mode cooperative threads were also popular at one time.
(We decided to not go there because cooperative parallelism sucks balls.)
But coroutines have overhead (so do threads). Your millions of coroutines might be worse than your thousands of threads in terms of making efficient use of the cpu cores your app or service has available.
Remember that concurrency is not parallelism, it's not about the number of threads being used.
So, let's say you need to show a "dialog" which, upon exit, should execute a piece of code (e.g. to show another piece of UI). Maybe that dialog can be invoked from multiple places, and the code to execute (after the dialog) changes accordingly.
You could do that via callbacks, but that is not very composable, and is generally messy.
Better solution: a Promise which gets resolved when the user exits the dialog! Composing multiple pieces of UI then becomes equivalent to composing multiple (async) function calls, you can pass output from one as the input to another, use exceptions to "circuit break" the UI flow, etc.
The result then contains if the user clicked OK, Cancel, [X] to close the modal or any other state transition that you may have added to that dialog. The recipient (not necessarily the caller that initiated the dialog, in case you chain the responses) can then act accordingly and presumably triggers a different action on OK then the other state transitions.
Coroutines are to me a brain-twisty way to make the most out of a single core, because your language doesn't support multithreading. For example: JavaScript with a single thread in the browser, Python with the GIL.
C++ has threads which ACTUALLY run in parallel on the CPU. Why bother complicating the language further?
Because coroutines are easier to think about than threads, and avoid entire (large) classes of bugs.
Because coroutines (typically) have a lower overhead than threads.
Because if you are IO bound and not CPU bound, and therefore you don't need to spread work across cores, threads may not provide a given workload with much, or any, benefit.
I'm working on a post right now covering this in detail, but I spent the last 2 hours being reminded that explaining concurrency stuff in simple terms is hard. :-D
IIRC, a thread takes up ~32kB or so worth of data in pthreads/Linux, or at least... somewhere around there. So on a rather cheap server with 64GB of RAM, you can easily fit 1-million threads. More expensive servers have 512GB, 1TB or more RAM and can easily handle this kind of load.
Of course, coroutines are even smaller/more efficient and can go into 10-million, 100-million or higher.
It does default to 1MB, but these sorts of things are configurable.
Cooperative async runtimes are obscenely efficient.
That being said, yeah, most async workflows are obtuse and difficult to follow and they force all consumers of your interface to also use the async primitives if they want the benefits. Go seems to be one of the few languages that does async/coroutines right.
See: https://eli.thegreenplace.net/2018/go-hits-the-concurrency-n...
I just looked up Kotlin's co-routines, and there are a number of annoying hoops you need to jump through:
1. Co-routines can only be built by calling runBlocking or coroutineScope.
2. Functions that are async buy should act like blocking calls in a co-routine need to be marked with `suspend`
3. There are a bunch of primitives you need to use like `launch` or `async`.
4. There are built-in options to make the co-routines lazy, but it makes the API feel sloppy.
5. Rather than locks, Kotlin encourages explicit thread dispatching. Some times you need to do this in Go too, like when using a C library, but in general it's rare.
Other than confining UI work to the main thread, I don't think I have seen thread confinement as an alternative to locks.
---
A quick search on SO gives this example of parallel decomposition. https://stackoverflow.com/questions/57662739/pattern-for-fet...
Starting several `async` tasks, then awaiting them is cleaner for me. But that's subjective.
---
Kotlin's structured concurrency handles the context stuff by default. When you handle those concerns (cancellation for example) in go, it's just as complex, but more verbose.
In general, if you like Kotlin, I can see why you'd like their approach to concurrency. Kotlin really likes a large standard library with generic blocks that act as syntactic sugar. I used to like that too, but as I've gotten older I've gotten lazier. Kotlin now how too much mental overhead for my old brain.
You can build exactly Go's coroutines API on top of these primitives, by encapsulating the Continuation object in Channel and Mutex types, and you would not have to touch the `suspend` keyword (which is sugar for CPS-transforming the function).
So, while you can have a similar syntax in Kotlin, the overall systems will have different semantics.
In contrast, the approach with explicit promises and await can be mapped all the way down to a C struct with a function and a data pointer representing a callback. That is, it is C ABI compatible, and thus it can be easily composed across any boundary that respects that ABI.
go func() {
runtime.LockOSThread()
// ...
}()
Is simpler than creating a thread in other languages.Context switching between tasks has a variable cost, because the implications of the TLB flush depend on the working set size of the switched-to task. If it doesn't touch much memory, the variable cost is low; if it touches a lot, the variable cost can be much, much higher than the register save/restore (fixed cost).
There's also the cost of cache eviction. Not something you have to manually manage, but it's a cost you pay nonetheless. Maybe.
Seems like different type of concern than what you need multiprocessing for.
If you have every tried to implement a complicated iterator you will find that you model a for loop in your head but that the iteration of the loop is controlled from outside using the ++ operator. The ++ operator allows the iterator loop to go one more time round. Normally you have to manage this with state stored in some object. With coroutines you can actually write a loop and co_yield from the middle of the loop to implement the ++ operator.
In our code we have an abstraction that wraps a set using a variant. Instead of implementing an iterator/begin/end based on which data the variant holds, we have an iter() method that unwraps the variant and loops through and co_yields. Since you can consume a generator in a for-each loop, you use it the exact same way (except calling iter).
In our case it doesn't save that much complexity, but it's definitely some. I could imagine more complicated cases where it is even more convenient.
However, async/await is real nice as well.
That said, overhead always needs to be considered; the overhead cost of using a CPP co-routine would be considerable for implementing a state machine that transitions rapidly.
There are a few CPU-intensive tasks but those are easy to isolate and send to a threadpool.
You could use threads instead, but then you risk getting in all sorts of concurrency problems. And if you synchronize everything, then great, you have reinvented coroutines. In fact, you can think of coroutines as threads with built-in synchronization, which may be how they are implemented.
Actual parallelism is surprisingly slow and resource heavy in practice. When everything is on one core, you keep things local in L3, L2, L1.
However, add on a 2nd core, then you suddenly have "ping ponging". Lets say core#0 has data in L1 cache, and now Core#1 needs to read-modify-write to it. That means the cores need to:
1: Core#1 begins to mark that cache-line as exclusive.
2. Core#0 needs to then respond by marking the line as invalid. Then Core#0 ejects the data from L1, and passes it to Core#1.
3. Core#1 can finally begin to work on the data.
4. If Core#0 uses the data again, Core#1 must do step#2 again.
--------
This is called "Ping-ponging". L1 cache read/writes is 1-nanosecond, but ping-pongs can be 30nanoseconds or slower, 30x slower for no reason.
You don't want to add another core to your problem unless you're _actually_ getting a speed benefit. Its more complex than it may seem at first glance. You very well could add a bunch of cores and then suddenly your program is way slower because of these kinds of issues.
Another problem: False sharing. Core#0 is working on "int x", and Core#1 is working on "int y", but x and y are on the same cacheline. So they have to ping-pong even though they never actually touch the same data.
If thread A is going to need to read all the data touched by thread B, then it's unclear why you've split the task across threads. If there are still good reasons, then probably pin A and B to core N (never pin anything to core 0, unrelated story), and let them interleave there.
If that doesn't make sense, then yep, you'll have to face the cost of ping-ponging.
Paralellise it via sharding and I get 80,958,379 transctions per second.
If I remove the randomness, I get 134,600,233 transactions per second. If I parallelise with 12 threads I get 931,024,042 transactions per second.
My point being, if you paralellise properly and do not use shared data, then you can boost performance by a multiple of the number of threads.
https://github.com/samsquire/multiversion-concurrency-contro...
For the technique I use for this scalability, see my journal entries on sharded structs and sharded integers
https://github.com/samsquire/ideas4#565-sharded-struct-pseud... https://github.com/samsquire/ideas4#608-sharded-integers
Callbacks exist to avoid the overhead of marshalling async results back to the original calling thread: instead of having the calling thread poll until the child thread(s) pass their results back, the calling function returns after kicking off the children, and one of the children directly resumes the work directly when the results are present. This is a significant performance gain.
However, callbacks have famously poor ergonomics, so many structured alternatives have been created: Promises/Futures, Async/Await, and CPP Coroutines.
You may wonder why OS threads are not themselves sufficient, but it turns out there is a decent amount of overhead involved in managing huge numbers of OS threads. There are limits at which Linux does not perform well anymore, leading to problems like live locking. Also, if you can avoid context switching/cache invalidation by keeping a single OS-level thread busy, you get performance benefits even before you hit OS limits.
So the userspace scheduling and green threading that coroutines enable allow you to increase concurrency without hitting these limits.
I can buy that. Think about a system thread. It's pretty much its own process that shares an address space with others. Its interface is syscalls and memory. A user thread (coroutine, goroutine, greenlet, whatever) is... whatever you want it to be. It can be small and special-purpose.
So saying that coroutines are low-level / not application code is misguided.
It's pretty easy to write a library with them (using your own executor) and not reveal their use to the user.
Obviously sharing an executor between application and library is another matter.
Especially when there is not one, but several libraries, and the application itself doesn't actually has or uses any executors, now that's a fun challenge.
Although doesn't that defeat the purpose of structured concurrency?
i.e. creating tasks that are plumbed to some background thread/executor.
By directly invoking the coroutine, lifetime issues become much easier to avoid.
Now the folly/experimental/coro library can be compiled by anything that claims to support C++20, which is a pretty big win for FB if nothing else.
1) stacklessness, which means coroutines can only yield from the top-level function that is called by the coroutine, sort of like the "simple generators" introduced in Python 2. Some people would consider these to not be real coroutines at all, and Python 3 later introduced actual (stackful) coroutines aka async functions. The C++ devs say that if their coroutines had stacks they would be called fibers instead of coroutines, but I had always thought fiber (used that way) was a Windows-specific term.
2. Opaqueness of the coroutine frame object: you can't get at its size or contents. That lets the compiler optimize its layout or even in some cases eliminate it. But at least for now, real compilers don't seem able to do that, and s0me LLVM devs aren't sure it's even feasible. Rust doesn't attempt this. It just uses an ordinary struct to hold the frame data, so you can do normal struct things with it.
Above is my impression from reading the linked article and some related ones. I have not yet tried to use C++ coroutines, or looked into Rust's even slightly. Protothreads (a C header library) are cool though. They don't do that much, but they are very simple in implementation compared to this C++ mess.
I think these C++ coroutines don't reach the usefulness of a lightweight cooperative Forth multitasker from 50 years ago. Those were stackful (typically a few dozen cells in each stack, so a task cost on a 16-bit cpu might have cost 200 or so bytes including stacks, user variables, and other data) and ridiculously simple.
Goroutines aren't comparable. They are way more powerful but also way more expensive than C++ coroutines.
There were apparently some other C++ coroutine proposals that sounded a lot better, but that didn't have the backing to get through the standardization process.
I don't know how Boost coroutines compare. I tried to use them once but didn't get them working.
https://stackoverflow.com/questions/74520133/how-can-i-pass-...
I recently ported the coroutine code from
https://blog.dziban.net/coroutines/
To GNU Assembler.
https://GitHub.com/samsquire/assembly see coroutines.S
I think the best combination is threads + coroutines with a IO event loop. This gives throughout for CPU tasks and IO scalability.
I'm excited that C++23 is getting `std::generator`, so we can finally write those simple loops: https://en.cppreference.com/w/cpp/coroutine/generator
The only reason I can see is lowering the overhead of creating threads, at the cost of harder to read controlflow.
Also, generators can be used to turn traversal functions into iterators, which is often tedious to do by hand.
We get all the numbers from 2 to 40, print out 2, then eliminate all the entries divisible by 2. Now we're left with 3, 5, 7, 9, etc.
We print out 3, then remove all the entries divisible by 3.
This repeats until we've hit the end (40). The next number after a round of eliminations will always be a prime.
e.g. if you have this code with boost:
#include <boost/asio.hpp>
#include <chrono>
#include <vector>
#include <thread>
#include <iostream>
std::mutex print_mutex;
int main()
{
using namespace boost::asio;
using namespace std::literals;
io_context io_context(20);
auto work_guard = make_work_guard(io_context);
for(int i = 0; i < 100; i++) {
post(io_context, [&] {
thread_local int x = 0;
int z = x++;
std::lock_guard _{print_mutex};
std::cout << z << "\n";
});
}
std::vector<std::jthread> v;
for(int i = 0; i < 20; i++)
v.emplace_back([&] { io_context.run_for(2s); });
}
i hope you don't expect the output to be "0, 1, 2, ..., 99"Stackful vs stackless comes with a few pro's and con's - e.g. it's often argued that stackless requires less memory. However stackful coroutines can allow for preemption at arbitrary points instead of just at yield points.
Stackful coroutines didn't require any compiler support, unlike stackless so they've be available for a long time (stackless have also been around since C++20).
Function coloring is a disadvantage of stackless (assuming it's mixed with regular, non-async C++ code). Async and non-async libraries can't interoperate the way they can with stackful. Arbitrary functions cannot block the calling coroutine with stackless, but they can with stackful. Some people consider the distinction a feature (a bit like checked exceptions, it's debated as to whether it's helpful or adds brittleness), but it's often a problem in large codebases that weren't written to be entirely async (including libraries) from the start. Anyway, if you want coloring as a distiction in your type system you can still have it. But with stackless, you have no choice.
Memory allocation patterns are different with stackless, sometimes worse. While a stackful coroutine system requires stacks to be allocated for each new coroutine, obviously, in a stackless async/await system there is typically a higher rate of memory allocation, and with varying sizes, to hold the temporary states of each coroutine which can occur at each await site. There are usually many more of those sites than coroutines.
In addition, those temporary states being stored in heap-allocated memory are likely to have a lower CPU cache hit ratio than stack memory during the run of a particular coroutine.
Something like the Linux kernel is very difficult to write in an explicit stackless style. The Linux kernel design uses stackful coroutines pervasively. This is not theoretical: It has come up in practice. Years ago there were a number of attempts to change the Linux filesystem and block I/O code to have async code paths (i.e. stackless style, state machines), so that a proper async I/O ("AIO") could be offered to userspace. Every attempt failed because it was too much work or too difficult to make all the filesystems code and everything they call, every path, fully async. Some of those changes went in, but the result was Linux AIO requests were not reliably async (and still are not), as they could sometimes block when they hit some less common paths in filesystem code, e.g. doing things like updating a block extent map, b-tree, or LVM/RAID corner case. In the end, the async I/O designs that worked well and reliably didn't block on request submission all ended up delegating I/O requests to scheduled stackful coroutines, i.e. kernel threads. These also turned out to perform well, which is not a coincidence, as stackful context switching is efficient. Of these designs, the one which ended up being adopted and well known is called io_uring.
For an example of less common paths that still need to be async, some operations have to allocate memory in all sorts of ways, including temporary memory when calling library functions or traversing data structures. In a kernel, memory allocation (i.e. malloc/new) has to be able to block the calling task temporarily, so that when there isn't enough free memory immediately available, the allocating task will wait for another task to free some, as that is preferable to failing an entire operation. Try doing that with C++ coroutines and idiomatic object allocation, and you will hit the function color problem: You can't async allocate a new object. You could of course write in a non-idiomatic style, not using new or constructors or anything which calls those, doing your own "await my_async_new" everywhere, but having to do that utterly consistently throughout a large codebase, and requiring every little function (including all libraries) to work that way as well, would be not really using C++ as it is meant to be used, and comes with its own risks. Alternatively you can block the thread the executor is running on, but that defeats the point of async coroutines.
With stackful coroutines, those kinds of operations Just Work(tm).
You can achieve the same thing with stackless by allowing such operations to block the executor thread, and spawn new executor threads which do work-stealing to ensure other coroutines are able to make progress while the first one is blocked. I believe that is what Go and Rust's Tokio do. The effect is to allow coroutines to be a mixture of stackless and stackful as needed, optimising for both worlds at the same time. It has the particular benefit of improving performance when the executor needs to call an operation which blocks in a library function or in the kerne. Howver, as with stackless, to ensure every coroutine can progress in an async manner without being stalled by coroutines that are blocked, this also needs every code path and library function to buy into doing that (if only by annotating "I may do something that will block now" regions). So it's also not suited for retrofitting to all codebases.