Capy Review, take 2
Rainer Deyke via Boost <[email protected]> Mon, 29 Jun 2026 12:23:33 +0200
| Newsgroups | gmane.comp.lib.boost.devel |
|---|---|
| Message-ID | <[email protected]> |
This is my second formal review of Capy, meant to replace the original,
which I am hereby formally withdrawing.
I based my previous reject verdict mainly on two factors:
1. The documentation of the IoAwaitable protocol does not match the
implementation, making it impossible to write working IoAwaitables.
This is a critical problem that has still not been fixed. However, it
is an eminently fixable problem that has been acknowledged, so I am
downgrading it to a conditional-acceptance-level flaw.
2. Capy is incomplete as a coroutine library. While this is true, it
misses the core of what Capy is supposed to be:
On 6/27/26 14:21, Vinnie Falco via Boost wrote:
> Capy is a proposed standard protocol for coroutine environment propagation,
> plus a reference implementation of that protocol.
In other words, I should have been focusing my review on the core
concepts, not on the bells and whistles. Bells and whistles can be
added by other libraries (and Corosio is one such library, even if it
not the one I am looking for), but core concepts must be correct from
the start.
So let's take another look at IoAwaitable and the
resume-on-same-executor guarantee.
+-------------------+
| About IoAwaitable |
+-------------------+
First off, the name is wrong. The concept has nothing to do with i/o.
I understand the history of the name, that the concept was written first
and foremost to support the corosio library as a replacement for ASIO,
both of which have io in the name, but it's not a descriptive name.
Renaming it is going to be disruptive, which is all the more reason to
rename it *now* instead of waiting for the rename to be forced when
library is standardized.
IoAwaitable exists to support the guarantee that every task runs on its
executor. This guarantee is useful for making it easier to reason about
code and for writing correct code. It also comes at a considerable
runtime cost. Coroutines get bounced around between IoAwaitables and
executors like a game of ping-pong.
It's not *wrong*, per se, to do it this way. It's a valid approach.
The benefits are worth the costs in many cases. But it's not the only
valid approach. And it seems like a shame that a supposedly universal
protocol like IoAwaitable forces these compromises on users without
escape hatches. So I would like to propose two escape hatches that
don't allow IoAwaitables to be bypassed, but work with them.
The first is resume_on. I realize that this has already rejected by the
Capy developers in favor of capy::run, but each capy::run call requires
an extra coroutine frame and an extra executor switch after the inner
coroutine co_returns. resume_on also allows the code running on the
alternate executor to directly use co_return for the main coroutine,
which provides better ergonomics. Compare:
task<int> f() {
co_return capy::run(ex)([] { co_return g(); });
}
task<void> f(int a) {
capy::resume_on(ex);
co_return g();
}
The second example is shorter in characters (though spread out over more
lines), and I find it much clearer. And this is a trivial example. The
difference becomes much more pronounced when the capy::run call nests
several layers deep.
One caveat about resume_on: its effect should be limited to the
coroutine in which it is used. When coroutine A co_awaits coroutine B,
and coroutine uses resume_on and then co_returns, execution on coroutine
A should always resume on A's original executor, not the executor that B
switched to.
I suspect that the problem with resume_on is ideological rather than
technical: it violates the rule that a coroutine must execute always run
on the same executor. If there are technical issues with it that make
in unpracticable, these should be documented. If there are no technical
issues, then the guarantee should be relaxed just a tiny bit to allow
resume_on while still keeping the guarantee in general.
The second escape hatch is immediate_executor. It looks something like
this:
class immediate_executor {
public:
std::coroutine_handle<> ce.dispatch(capy::continuation &c) {
// Obey the letter of the law by not just returning c.h...
this->post(c);
return {};
}
void post(capy::continuation &c) {
// ...but violate the spirit of the law by calling h.resume().
c.h.resume();
}
// ...other functions here...
};
immediate_executor is a powerful way for opting out of costs, and the
benefits, of more robust executors where they are not needed or wanted.
And it appears to be completely legal. It works great in conjunction
with resume_on, but it also reduces the cost of capy::run significantly,
since switching to an immediate_executor is effective free.
Put big red warning signs on immediate_executor that it should only be
used by experts who know what they are doing if you have to, but provide it.
+---------------------------------------+
| About the Documentation for Beginners |
+---------------------------------------+
The Capy documentation starts with an introduction to C++ coroutines.
This tells beginners how to use C++ coroutines. It does not tell
beginners how to *safely* use C++ coroutines, because that's apparently
intermediate-level knowledge. In particular, there are no warning about
the unfortunate interactions between thread synchronization structures
and coroutines running in thread pools that I wrote about extensively in
my original review.
You might say that it's not Capy's business to educate the users on
basic coroutine safety. I say that Capy made it its business when it
included a basic coroutine primer in its documentation.
+---------------------------+
| About Strands and Mutexes |
+---------------------------+
When ASIO introduced strands, it was a revolutionary alternative to
std::mutex. But the alternative to capy::strand is not std::mutex but
capy::async_mutex, and they are more alike than they are different.
Basic code that uses one can easily be rewritten to use the other:
task<void> f() {
for (;;) {
capy::run(my_strand)([]{ /* critical section */ });
// Safe code here.
}
}
task<void> f() {
for (;;) {
{
auto [ec, guard] = co_await my_mutex.scoped_lock();
// Apparently locking can fail for mutexes but not for strands?
// Or do strands just report failure differently?
if (ec) { co_return; }
// Critcal section
}
// Safe code here
}
}
I can't imagine that their implementations are all that different
either. Maybe one is faster than the other, but it's not clear from the
documentation which one it is.
There are things you can do with capy::async_mutex that you can't do
with capy::strand, but most of the examples that come to my mind are
stupid and dangerous. Conversely, the only thing that comes to mind
that you can do with capy::strand but not with capy::async_mutex is to
directly post tasks to the strand, skipping an unnecessary co_await.
Can they be merged? Should I prefer one over the other? Can some text
be added to the documentation to help me choose between them?
+---------------------+
| Summary and Verdict |
+---------------------+
Despite its origins as a dumping ground for the parts of Corosio that
don't need sockets, Capy is a very ambitious and useful in its own right
and I have been reviewing it as such. I have focused my review on what
I see as the heart of Capy to avoid unnecessary bikeshedding. I might
not like the stream and buffer concepts, but I don't have to use them.
I vote to conditionally accept Capy into Boost. I think that as a
fundamental coroutine library, Capy fills a major gap in the standard
library. I also think it's important that Capy gets battle-tested as
part of Boost before it is adapted by the standard library. And I think
that its known major flaws should be addressed before it makes it into
Boost.
1. The documentation of IoAwaitable is still broken because it does not
use capy::continuation. I am told that this will be fixed. It hasn't
been fixed yet. It needs to fixed before Capy is accepted into Boost.
2. Either provide resume_on, or provide a very good rationale for why it
cannot or must not be implemented. Because without one or the other, it
will be implemented, badly, by the users.
3. Either provide immediate_executor, or provide a very good rationale
for why it cannot or must not be implemented. Because without one or
the other, it will be implemented, badly, by the users.
4. Either provide a good explanation of the pitfalls of using
coroutines, especially in conjunction with thread synchronization
structures, or declare Capy an "experts-only" library that presumes this
knowledge and cut the introduction to coroutines section entirely.
(Feel free to use my previous review as a reference of what some of
these pitfalls are.)
5. There is duplicate functionality between capy::strand and
capy::async_mutex. Either fix this, or provide documentation that
acknowledges that this duplication exists, explains why it exists, and
provides guidelines on when to use which one of the alternatives.
Note that every single one of these can potentially be fixed by just
changing the documentation. I haven't tested the implementation, but I
assume it's fine. And if its flawed, then I assume that it can and will
be fixed.
--
Rainer Deyke - [email protected]
_______________________________________________
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/KJQIZK3PXEYFWLNHSHZZE5VFENRVHO77/