Page 1 of 1

C++26 Async

Posted: Tue Oct 21, 2025 9:55 am
by bellezzasolo
Just wondering what peoples' thoughts on this are?

I've had a bit of a play with the concept, and the NVIDIA stdexec reference implementation. It added about 20KB to an embedded project, which looks like a lot of overhead when you're dealing with a 64KB binary... however, it definitely seems interesting to explore further.

Schedulers, senders, receivers, and a potential for allocation-free asynchronous code. Plus interop with coroutines.

Re: C++26 Async

Posted: Mon Nov 10, 2025 3:45 pm
by AndrewAPrice
I went with fibers, mainly because I don't find the C++ coroutines syntax to be very elegant. Maybe that will change with C++26 and beyond. The Nvidia example is looking nice.

It's been years, but I believe I got started by reading a blog post like this: https://graphitemaster.github.io/fibers/

My implementation:
(fibers.h, fibers.asm, fibers.cc, scheduler.h, scheduler.cc).

Usage is:

Code: Select all

Defer([](){ /* Do something in a fiber. */ });
I haven't built a fiber friendly future or promise, but I will when I have a use for it (currently I use callback functions for RPCs in my microkernel-based OS, and the callbacks run inside of a fiber that gets added to a queue).

The cool thing is you can recycle the fibers to avoid allocations, you only have to save the callee preserved registers, and you don't require kernel support.

Re: C++26 Async

Posted: Wed Nov 12, 2025 4:51 pm
by Candy
I've looked into using C++20's coroutines in an OS context a while ago, not with C++26's or P2300's sender/receiver setup. I got it to work on an emulator and IIRC a RPI3 enough to handle interrupts as coroutines. In the setup you start your interrupt handler by calling the function normally, and it then co_await's the interrupt itself being triggered, and then handles it in a while(true) loop. You can see it in https://github.com/dascandy/bookish-pot ... d.cpp#L196 starting the USB HID handler (which handles the reports), and the future itself being ignored in line 276.

https://github.com/dascandy/bookish-potato

The OS basically does fibers everywhere; it doesn't have any kind of preemptive multitasking. It does have a shell that can handle some commands - each character you type is a co_await for the stdin returning a new character, which is filled from things like the HID report above. It works pretty well for what it is so far.

Re: C++26 Async

Posted: Thu Nov 13, 2025 3:58 pm
by AndrewAPrice
I am fascinated by C++ co-routines, I just haven't found a practical use for them over fibers.

For example, a simple `std::cout <<` could cause the buffer to flush, which may mean in a microkernel environment, the program needs to issue an RPC to the listener of 'stdout' to handle the buffer and block until it is done and can be recycled.

Using coroutines, you need to co_await all the way up your call-stack, and have a top level scheduler loop through all non-sleeping co-routines. With fibers, you can silently switch to another running fiber, and execution just continues once it's done.

I like the "do work while iterating" pattern, e.g. forEachItem([](const Item& item) { ... }) - I could see coroutines being useful in this circumstance since you could execute a small batch at a time.

Re: C++26 Async

Posted: Fri Nov 14, 2025 2:31 am
by bellezzasolo
AndrewAPrice wrote: Thu Nov 13, 2025 3:58 pm I am fascinated by C++ co-routines, I just haven't found a practical use for them over fibers.

For example, a simple `std::cout <<` could cause the buffer to flush, which may mean in a microkernel environment, the program needs to issue an RPC to the listener of 'stdout' to handle the buffer and block until it is done and can be recycled.

Using coroutines, you need to co_await all the way up your call-stack, and have a top level scheduler loop through all non-sleeping co-routines. With fibers, you can silently switch to another running fiber, and execution just continues once it's done.

I like the "do work while iterating" pattern, e.g. forEachItem([](const Item& item) { ... }) - I could see coroutines being useful in this circumstance since you could execute a small batch at a time.
I think the downside of the fiber approach there is that each fiber has to have its own call stack. Stack switching then has cache implications, too, although thread affinitisation will definitely help alleviate this.

C++20 stackless coroutines instead have a frame, storing variables needed across awaits. It's a lighter weight mechanism, and I saw a talk where they used co_await to queue up memory prefetches... memory as an asynchronous resource!

However, coroutine frames basically have to be heap allocated is my understanding.

C++26 senders and receivers can be stack allocated, but also co_await'ed. For advanced scenarios, zero heap allocation is possible.

Re: C++26 Async

Posted: Mon Nov 17, 2025 2:25 pm
by AndrewAPrice
bellezzasolo wrote: Fri Nov 14, 2025 2:31 am each fiber has to have its own call stack.
It is possible to recycle a pool of stacks (which I do). It's slightly less memory efficient but no heap allocation is needed unless the pool is empty. This doesn't help with the cache invalidation on stack-switch though. Also I only have to push/pop 7 callee-saved registers to switch fibers, which I find super efficient.

Re: C++26 Async

Posted: Tue Nov 18, 2025 2:53 am
by bellezzasolo
AndrewAPrice wrote: Mon Nov 17, 2025 2:25 pm
bellezzasolo wrote: Fri Nov 14, 2025 2:31 am each fiber has to have its own call stack.
It is possible to recycle a pool of stacks (which I do). It's slightly less memory efficient but no heap allocation is needed unless the pool is empty. This doesn't help with the cache invalidation on stack-switch though. Also I only have to push/pop 7 callee-saved registers to switch fibers, which I find super efficient.
I mean, it does sound like the fundamentals are the same, it will be the same with callee saved registers for any cooperative async (unlike my kernel driver model at the moment which uses event threads and semaphores).

Stackful coroutines will have better locality of reference. Dynamic allocation for co_await can be resolved similarly with a slab allocator or something, but will be storing local variables that have to be persisted across await boundaries in one place (the frame), whereas simple locals will be on the thread stack. However, that does have a benefit in terms of cache, the thread stack will be hot.

Senders/receivers could put the frames on the stack, if you can make lifetime guarantees about the stack frame. In a simple case, that's creating the task graph and running the scheduler.

In a kernel, you could envision a driver having one thread and running its asyncs, but then you get context switches (although, in a microkernel, this isn't an additional disadvantage). Or you can have a thread pool running a task graph that's probably dynamically allocated.