Re: Capy Review

Peter Dimov via Boost <[email protected]> Fri, 26 Jun 2026 18:57:26 +0300
Newsgroups gmane.comp.lib.boost.devel
Message-ID <[email protected]>
Rainer Deyke wrote:
> On 6/25/26 16:58, Peter Dimov via Boost wrote:
> > The problem is that there needs to be a standard way to propagate
> > important state to the foreign coroutine, so that the foreign
> > coroutine can at least propagate that same state back to you if it co_awaits
> one of yours.
> >
> > That state in Capy consists of the executor, the stop token, and the allocator.
> > And while you can argue that the allocator is optional (but it isn't
> > in practice), the other two most certainly are not.
> 
> Let's call this the "A co_awaits B, B co_awaits C" problem, where A and C use
> the same coroutine library.  This is distinct from the "A co_awaits B, B resumes
> A" problem, where the awaitable B attempts to return control back to A after
> suspending.
> 
> This problem has solutions.

It does, but they aren't good ones.

Let's first address the executor. The programming model where coroutines
resume on their original executor, as opposed to a random one, is much
easier to reason about, much less error prone, and allows much simpler code.

If you know that your I/O coroutine will always resume on your singe threaded
I/O executor, this allows you to skip all locking. (*) It also avoids your coroutine
borrowing a thread pool thread that is intended for CPU intensive tasks; this
is suboptimal. (And conversely, a CPU intensive task ending up on the single
threaded I/O executor would be a disaster, although this would be much less
likely to happen.)

(*) It also allows you to use Boost.Leaf, to borrow some ammo from the
adjacent thread.

So in general, we would like to be able to support this "coroutine always
resumes on its original executor" model.

"This problem has solutions."

Indeed. The first solution is for the coroutine author to manually switch
back to the original executor after each co_await. Needless to say, this
quickly becomes verbose, repetitive, boring and ultimately unworkable.

The second solution is to automate this and make the promise insert
the switch back automatically in await_transform. This works, but requires
the allocation of a coroutine frame on each co_await, which is suboptimal
and unnecessary if we know that the co_awaited coroutine will resume on
the original executor.

The third solution is to devise a way to mark the coroutines that already
resume on the original executor in some way (this is the IoAwaitable
protocol), and then, if the co_awaited routine is one of those, avoid
wrapping it in another frame.

This is essentially the path that led to the current Capy design.

> The verbose, explicit option to carry the auxiliary objects we need as function
> arguments.  I actually like this option for stop tokens.  It means that each
> co_await makes it explicit if the awaitable in question
> (potentially) uses the stop token or the allocator.  It means being able to
> substitute in a different stop token or allocator at the call point.
> (To be fair, Capy offers just that functionality for stop tokens, through
> boost::capy::run.)

That's possible but, again, verbose, repetitive, and error prone. Since at
this point we're at our third solution above, where we already have a
protocol for passing state from A to B to C, it's natural to just put the
stop token there as well, and avoid all that manual handling.

And now we arrive at our next juncture. We can co_await our IoAwaitables
with everything being passed down splendidly, and we can also co_await
foreign awaitables, which we wrap so that we resume on our original
executor. This allows foreign awaitables to work, but we lose all the
propagated context.

This is kind of fine when everything is written correctly; it does exactly
what needs to be done. But it's error prone.

We have to pass the additional state to the foreign awaitable. Maybe it
does have an overload taking a stop token as a parameter, and maybe
we can actually call it the right way and have everything working. But if
we forget, everything still compiles and works, except that the stop token
is silently dropped, and the coroutine becomes uncancelable.

Similarly, maybe the author of the foreign awaitable did actually intend
to implement IoAwaitable, but got the signature subtly wrong. Again,
everything still compiles and works, but we get an unnecessary coroutine
frame and all the additional context is silently dropped.

So... it's a tradeoff and the (right in my opinion) call here is to move to
our fourth solution and require IoAwaitable. This way, the above failure
modes are compile errors.

Yes, it's less permissive, and yes, it requires everyone to implement
IoAwaitable. This is a downside today, but the ambition here is to figure
out what the de-facto standard state propagation protocol needs to be,
test it in practice, then eventually, codify if as de-jure.

"This all should have been in the documentation."

Not going to argue with you here. It absolutely should have been.

> (Note that even in Capy, stop tokens require manual checking to work.
> It is entirely possible to write uncancelable coroutines or other IoAwaitables in
> Capy.  And cancellation may not be necessary if a coroutine is fast enough, and
> guaranteed to stay fast enough.  Not accepting a stop token as an argument
> makes it explicit if this is intentional, and accepting a stop token as an
> argument makes it less likely to happen unintentionally because the argument
> is there to remind
> you.)
> 
> The pragmatic option is to simply create a new context at the point where C is
> called.  C has a stop token, but it's distinct from A's stop token.  If we don't
> want that, we can manually pass the stop token through B.

A new token is not what we want. The stop token supports the basic when_any
primitive and we really want everything co_awaited from this when_any call
downwards to use the same stop token so that when_any can do its job.


_______________________________________________
Boost mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://lists.boost.org/mailman3/lists/boost.lists.boost.org/
Archived at: https://lists.boost.org/archives/list/[email protected]/message/4IQQNZEUHGSJ6QIHPJTWXBFFV5E7PQUS/