This review has surfaced a documentation problem that I want to address directly. Several reviewers have arrived at different (incompatible) conclusions about what Capy is, because we never stated it plainly. Let me try now. *What Capy is* Capy is a proposed standard protocol for coroutine environment propagation, plus a reference implementation of that protocol. The protocol (IoAwaitable) ensures that three things flow correctly through co_await chains: executor, stop token, and allocator. The concrete library - thread pool, task types, byte streams, synchronization primitives - exists to prove the protocol works in practice. *The invariant* Capy enforces one rule: A coroutine always resumes on the executor it was launched with. This isn't a restriction for its own sake. Consider: capy::task<void> handle_client(connection& conn) { auto req = co_await conn.read(); auto resp = process(req); co_await conn.write(resp); conn.stats.requests++; } Launch this on a strand. Every resumption after co_await happens on that strand. conn.stats.requests++ is data-race-free without a mutex. Correct by construction. Without the invariant, after co_await conn.read() you might resume on an io_uring completion thread, a random pool thread, anywhere the I/O subsystem completed. Now you need either a mutex around every access to shared state, or a manual co_await resume_on(my_strand) after every co_await. The first defeats strands. The second is verbose, repetitive, and if you forget one, you have a silent data race. The invariant makes the correct thing automatic and the incorrect thing a compile error. *Why IoAwaitable, not plain awaitables* A plain awaitable can resume a coroutine on any thread by calling coroutine_handle::resume() directly. That breaks the invariant. The compile error when you co_await a plain awaitable from a Capy coroutine is the type system preventing this. This isn't about locking you in. It's about making environment propagation explicit. An IoAwaitable receives the io_env (executor + stop token + allocator) and promises to dispatch the resumption through the executor. That promise is what enables the safety guarantee. *The interop problem* Without a shared protocol, N coroutine libraries need N*(N-1) adapters. With a standard protocol for environment propagation, one bridge covers everyone. IoAwaitable is our proposal for that protocol. Capy is the testbed. The papers (P4172, P4092, P4093) are the standardization path. *What Capy is not* Capy is not a general-purpose "do everything" coroutine framework competing with TooManyCooks feature-for-feature. It is not an implementation detail of Corosio. It is the execution model and byte-stream layer - usable standalone for business logic that operates on streams without platform I/O (HTTP parsing, protocol state machines, serialization), and usable as the foundation for Corosio's networking layer. CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines. The Boost.HTTP parser is implemented entirely on Capy's byte streams. These are the intended use cases for Capy alone. *What went wrong in this review* The documentation never stated any of the above. It explained the API without explaining the design's purpose. Rainer encountered a type-safety check, found no documentation for the escape hatch, and concluded the escape hatch didn't exist. The frustration was understandable. The documentation failed him. We are fixing this. Bikeshed ideas: Cowaitable, Exwaitable Thanks
sob., 27 cze 2026 o 14:22 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
This review has surfaced a documentation problem that I want to address directly. Several reviewers have arrived at different (incompatible) conclusions about what Capy is, because we never stated it plainly. Let me try now.
*What Capy is*
Capy is a proposed standard protocol for coroutine environment propagation, plus a reference implementation of that protocol.
The protocol (IoAwaitable) ensures that three things flow correctly through co_await chains: executor, stop token, and allocator. The concrete library - thread pool, task types, byte streams, synchronization primitives - exists to prove the protocol works in practice.
*The invariant*
Capy enforces one rule:
A coroutine always resumes on the executor it was launched with. This isn't a restriction for its own sake. Consider:
capy::task<void> handle_client(connection& conn) { auto req = co_await conn.read(); auto resp = process(req); co_await conn.write(resp); conn.stats.requests++; }
Launch this on a strand. Every resumption after co_await happens on that strand. conn.stats.requests++ is data-race-free without a mutex. Correct by construction.
Without the invariant, after co_await conn.read() you might resume on an io_uring completion thread, a random pool thread, anywhere the I/O subsystem completed. Now you need either a mutex around every access to shared state, or a manual co_await resume_on(my_strand) after every co_await. The first defeats strands. The second is verbose, repetitive, and if you forget one, you have a silent data race.
The invariant makes the correct thing automatic and the incorrect thing a compile error.
*Why IoAwaitable, not plain awaitables*
A plain awaitable can resume a coroutine on any thread by calling coroutine_handle::resume() directly. That breaks the invariant. The compile error when you co_await a plain awaitable from a Capy coroutine is the type system preventing this.
This isn't about locking you in. It's about making environment propagation explicit. An IoAwaitable receives the io_env (executor + stop token + allocator) and promises to dispatch the resumption through the executor. That promise is what enables the safety guarantee.
*The interop problem*
Without a shared protocol, N coroutine libraries need N*(N-1) adapters. With a standard protocol for environment propagation, one bridge covers everyone. IoAwaitable is our proposal for that protocol. Capy is the testbed. The papers (P4172, P4092, P4093) are the standardization path.
*What Capy is not*
Capy is not a general-purpose "do everything" coroutine framework competing with TooManyCooks feature-for-feature. It is not an implementation detail of Corosio. It is the execution model and byte-stream layer - usable standalone for business logic that operates on streams without platform I/O (HTTP parsing, protocol state machines, serialization), and usable as the foundation for Corosio's networking layer.
CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines. The Boost.HTTP parser is implemented entirely on Capy's byte streams. These are the intended use cases for Capy alone.
*What went wrong in this review*
The documentation never stated any of the above. It explained the API without explaining the design's purpose. Rainer encountered a type-safety check, found no documentation for the escape hatch, and concluded the escape hatch didn't exist. The frustration was understandable. The documentation failed him.
We are fixing this.
Bikeshed ideas: Cowaitable, Exwaitable
So it looks to me that Capy has at least two distinct parts with distinct purposes. One is the proposed universal API for associating awaitables with their execution environment, and for propagating this environment through coroutines. One that hopefully every library dealing with awaitiables would want to adapt. It comprises: * concepts IoAwaitable, IoRunnable, Executor, ExecutionContext * type-erased executor * synchronization primitives, strands * functions such as when_any, when_all * task<> The other is "stream interfaces". Not stream implementations: just interfaces. It uses the former part. It comprises: * buffers * stream concepts * type erasure for streams * stream-related algorithms, such as read_until. The former one -- let's call it Awaitables -- is meant to be a vocabulary type: a type which different libraries recognize and through which they communicate. One might want to use it without using the other one (let's call it AsyncStreams) precisely for this purpose: to combine two libraries. The Streams are just an API. Someone might want to include it in order to provide a library of algorithms for consuming streams. Someone else might want to use it to *provide* their own stream implementation. Someone who needs a program doing I/O will need Streams + some other library. This library could be Corosio or it could be something else. So, you want just the Awaitables protocol? Use a subset of Capy. You want an ASIO replacement? Use Capy + Corosio. Maybe Awaitables should be its own library? If not, maybe at least have the documentation in Capy split into two distinct sections, and not mix them in the Reference section. Maybe use different namespaces. Regards, &rzej;
On Sat, Jun 27, 2026 at 7:29 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
So, you want just the Awaitables protocol? Use a subset of Capy. You want an ASIO replacement? Use Capy + Corosio.
Right
Maybe Awaitables should be its own library?
The include are structured that way, to some extent, through the subdirectories in include/boost/capy. However this point to a tension in the Boost. There are competing forces: * Users want as few dependencies as possible * Users also don't like the monolithic distribution * Good engineering means decomposition of concerns, but * each library has to pass a review For the last point above, note that even though Corosio and Capy were presented as a bundle, Capy was evaluated independently, and the Capy portion was rejected by one reviewer. In practical terms the more finely decomposed an offering into individual libraries, the harder it is to present and pass review.
If not, maybe at least have the documentation in Capy split into two distinct sections, and not mix them in the Reference section.
Documentation can always be improved, yes Thanks
sob., 27 cze 2026 o 16:58 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
However this point to a tension in the Boost. There are competing forces:
* Users want as few dependencies as possible * Users also don't like the monolithic distribution * Good engineering means decomposition of concerns, but * each library has to pass a review
sob., 27 cze 2026 o 17:15 Peter Dimov via Boost <boost@lists.boost.org> napisał(a):
It's more than two parts. The second part is actually
1. Buffer sequences
* ConstBufferSequence, MutableBufferSequence concepts * supporting primitives: buffer_begin, buffer_end, buffer_size, buffer_length, buffer_slice
2. Streams, stream-related algorithms
* read, write
3. Type erasure
4. DynamicBuffer
Here (1) has nothing to do with coroutines and is completely independent.
A number of division possibilities, I see. However, if it is really the goal to establish a "bridge component" between different awaitable-style coroutine libraries, then coupling it with another library (which is a bridge between different I/O libraries) reduces the chances of success, as we have already seen during this review. Regards, &rzej;
Andrzej Krzemienski wrote:
sob., 27 cze 2026 o 16:58 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
However this point to a tension in the Boost. There are competing forces:
* Users want as few dependencies as possible * Users also don't like the monolithic distribution * Good engineering means decomposition of concerns, but * each library has to pass a review
sob., 27 cze 2026 o 17:15 Peter Dimov via Boost <boost@lists.boost.org> napisał(a):
It's more than two parts. The second part is actually
1. Buffer sequences
* ConstBufferSequence, MutableBufferSequence concepts * supporting primitives: buffer_begin, buffer_end, buffer_size, buffer_length, buffer_slice
2. Streams, stream-related algorithms
* read, write
3. Type erasure
4. DynamicBuffer
Here (1) has nothing to do with coroutines and is completely independent.
A number of division possibilities, I see. However, if it is really the goal to establish a "bridge component" between different awaitable-style coroutine libraries, then coupling it with another library (which is a bridge between different I/O libraries) reduces the chances of success, as we have already seen during this review.
Both coroutine support and buffer sequence manipulation are foundational for Corosio (and it's not really possible to omit either), so for me both clearly belong in Capy. Streams (and the read/write algorithms) are theoretically possible to omit, but doing so will be a disservice to users and if we do that, it will very likely lead to pressure to add read/write as socket methods.
On Sat, Jun 27, 2026 at 5:23 PM Peter Dimov via Boost <boost@lists.boost.org> wrote:
Both coroutine support and buffer sequence manipulation are foundational for Corosio (and it's not really possible to omit either), so for me both clearly belong in Capy.
Streams (and the read/write algorithms) are theoretically possible to omit, but doing so will be a disservice to users and if we do that, it will very likely lead to pressure to add read/write as socket methods.
This surfaces a recurring issue. If you recall, I had this broken up into 6 libraries. Buffers was separate, I forget what the other subdivisions were. A common complaint is that Boost is too large. And people want just some parts without the whole thing. And then we have the desire to not have too many dependencies. In principle buffers and streams should be 2 separate libraries from IoAwaitable. Buffers you can use without either of the other two. IoAwaitable could be used without buffers and streams, which covers Rainer's case. What is the right split? I'm comfortable with how we have it now, Capy and Corosio, but this reflects a political tradeoff instead of being purely technical. Thanks
Andrzej Krzemienski wrote:
So it looks to me that Capy has at least two distinct parts with distinct purposes.
One is the proposed universal API for associating awaitables with their execution environment, and for propagating this environment through coroutines. One that hopefully every library dealing with awaitiables would want to adapt.
It comprises: * concepts IoAwaitable, IoRunnable, Executor, ExecutionContext * type-erased executor * synchronization primitives, strands * functions such as when_any, when_all * task<>
The other is "stream interfaces". Not stream implementations: just interfaces. It uses the former part. It comprises: * buffers * stream concepts * type erasure for streams * stream-related algorithms, such as read_until.
It's more than two parts. The second part is actually 1. Buffer sequences * ConstBufferSequence, MutableBufferSequence concepts * supporting primitives: buffer_begin, buffer_end, buffer_size, buffer_length, buffer_slice 2. Streams, stream-related algorithms * read, write 3. Type erasure 4. DynamicBuffer Here (1) has nothing to do with coroutines and is completely independent.
1. Buffer sequences
* ConstBufferSequence, MutableBufferSequence concepts * supporting primitives: buffer_begin, buffer_end, buffer_size, buffer_length, buffer_slice
Incidentally, `buffer_begin` is not named `buffer_begin` in Capy, but `begin`. This is a mistake. If `x` is `span<byte>` and we decide that spans are buffers, `begin(x)` is `x.begin()`, but `buffer_begin(x)` is `&x`. These two operations are not the same thing, so they should not be called with the same name.
sob., 27 cze 2026 o 14:22 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
This review has surfaced a documentation problem that I want to address directly. Several reviewers have arrived at different (incompatible) conclusions about what Capy is, because we never stated it plainly. Let me try now.
*What Capy is*
Capy is a proposed standard protocol for coroutine environment propagation, plus a reference implementation of that protocol.
The protocol (IoAwaitable) ensures that three things flow correctly through co_await chains: executor, stop token, and allocator. The concrete library - thread pool, task types, byte streams, synchronization primitives - exists to prove the protocol works in practice.
Actually, I do not think I understand this statement. What does it mean when you call the concept `IoAwaitable` a protocol? 1. What is this concept for? Are programmers/other libraries allowed/expected to provide their own types modelling `IoAwaitable`? Or does anyone is required to only use concrete types that come with Capy? 2. If it is the latter, why are we even hearing about this protocol? If it is the former, then I think the "protocol" is way underspecified. 3. I can write my own coroutine return type that has two-argument await_suspend, but lacks await_ready and await_resume. It will pass the IoAwaitable check, but I cannot use it inside a coroutine with capy::task as a return type. Apparently, there are more syntactic requirements here. 4. And IoAwaitable doesn't mention any semantic requirements. Concepts without semantic requirements are not concepts. 5. Is it enough for my type to satisfy the concept or is there more that I have to do? How do I make `co_await this_coro::environment` work? Is it even part of the "IoAwaitable protocol"? How is the environment propagated down to other people's coroutines (with their own return types) unless I manually make it work? 6. What happens with my promise_type's initial_suspend and final_suspend? Are they invoked or overridden by capy::tasks's await_transform? I hope that questions demonstrate how unclear it is to the reader what "IoAwaitable protocol" is. Regards, &rzej;
*The invariant*
Capy enforces one rule:
A coroutine always resumes on the executor it was launched with. This isn't a restriction for its own sake. Consider:
capy::task<void> handle_client(connection& conn) { auto req = co_await conn.read(); auto resp = process(req); co_await conn.write(resp); conn.stats.requests++; }
Launch this on a strand. Every resumption after co_await happens on that strand. conn.stats.requests++ is data-race-free without a mutex. Correct by construction.
Without the invariant, after co_await conn.read() you might resume on an io_uring completion thread, a random pool thread, anywhere the I/O subsystem completed. Now you need either a mutex around every access to shared state, or a manual co_await resume_on(my_strand) after every co_await. The first defeats strands. The second is verbose, repetitive, and if you forget one, you have a silent data race.
The invariant makes the correct thing automatic and the incorrect thing a compile error.
*Why IoAwaitable, not plain awaitables*
A plain awaitable can resume a coroutine on any thread by calling coroutine_handle::resume() directly. That breaks the invariant. The compile error when you co_await a plain awaitable from a Capy coroutine is the type system preventing this.
This isn't about locking you in. It's about making environment propagation explicit. An IoAwaitable receives the io_env (executor + stop token + allocator) and promises to dispatch the resumption through the executor. That promise is what enables the safety guarantee.
*The interop problem*
Without a shared protocol, N coroutine libraries need N*(N-1) adapters. With a standard protocol for environment propagation, one bridge covers everyone. IoAwaitable is our proposal for that protocol. Capy is the testbed. The papers (P4172, P4092, P4093) are the standardization path.
*What Capy is not*
Capy is not a general-purpose "do everything" coroutine framework competing with TooManyCooks feature-for-feature. It is not an implementation detail of Corosio. It is the execution model and byte-stream layer - usable standalone for business logic that operates on streams without platform I/O (HTTP parsing, protocol state machines, serialization), and usable as the foundation for Corosio's networking layer.
CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines. The Boost.HTTP parser is implemented entirely on Capy's byte streams. These are the intended use cases for Capy alone.
*What went wrong in this review*
The documentation never stated any of the above. It explained the API without explaining the design's purpose. Rainer encountered a type-safety check, found no documentation for the escape hatch, and concluded the escape hatch didn't exist. The frustration was understandable. The documentation failed him.
We are fixing this.
Bikeshed ideas: Cowaitable, Exwaitable
Thanks _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/XTGEPSMU...
On Sun, Jun 28, 2026 at 11:25 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
Actually, I do not think I understand this statement. What does it mean when you call the concept `IoAwaitable` a protocol?
A protocol in this context means the type requirements plus the behavioral requirements.
1. What is this concept for? Are programmers/other libraries allowed/expected to provide their own types modelling `IoAwaitable`? Or does anyone is required to only use concrete types that come with Capy?
There are three tiers of users identified in the companion design document IoAwaitable for Coroutine-Native Byte-Oriented I/O https://isocpp.org/files/papers/P4172R1.pdf 1. Application developers 2. Framework authors 3. I/O library authors Framework authors and I/O library authors would provide their own types modeling IoAwaitable. Users would simply consume the ones in Capy, or the ones provided by other libraries. 2. If it is the latter, why are we even hearing about this protocol? If it
is the former, then I think the "protocol" is way underspecified.
Capy serves the three audiences described above.
3. I can write my own coroutine return type that has two-argument await_suspend, but lacks await_ready and await_resume. It will pass the IoAwaitable check, but I cannot use it inside a coroutine with capy::task as a return type. Apparently, there are more syntactic requirements here.
The IoAwaitable documentation states: "An awaitable satisfies IoAwaitable if..." An IoAwaitable must be an awaitable, which requires at a minimum the three members described.
4. And IoAwaitable doesn't mention any semantic requirements. Concepts without semantic requirements are not concepts.
https://develop.capy.cpp.al/capy/reference/boost/capy/IoAwaitable.html#_sema...
5. Is it enough for my type to satisfy the concept or is there more that I have to do? How do I make `co_await this_coro::environment` work? Is it even part of the "IoAwaitable protocol"? How is the environment propagated down to other people's coroutines (with their own return types) unless I manually make it work?
"co_await this_coro::environment" is a feature of the task type. It has nothing to do with the IoAwaitable protocol, hence it is silent on it. Environments are propagated automatically to any awaitable which implements the IoAwaitable protocol, that's the point of it.
6. What happens with my promise_type's initial_suspend and final_suspend? Are they invoked or overridden by capy::tasks's await_transform?
I believe initial_suspend and final_suspend are invoked directly by the compiler and are not affected (Klemens?)
I hope that questions demonstrate how unclear it is to the reader what "IoAwaitable protocol" is.
Well... :) Thanks
niedz., 28 cze 2026 o 20:39 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
On Sun, Jun 28, 2026 at 11:25 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
Actually, I do not think I understand this statement. What does it mean when you call the concept `IoAwaitable` a protocol?
A protocol in this context means the type requirements plus the behavioral requirements.
1. What is this concept for? Are programmers/other libraries allowed/expected to provide their own types modelling `IoAwaitable`? Or does anyone is required to only use concrete types that come with Capy?
There are three tiers of users identified in the companion design document
IoAwaitable for Coroutine-Native Byte-Oriented I/O https://isocpp.org/files/papers/P4172R1.pdf
1. Application developers 2. Framework authors 3. I/O library authors
Framework authors and I/O library authors would provide their own types modeling IoAwaitable. Users would simply consume the ones in Capy, or the ones provided by other libraries.
2. If it is the latter, why are we even hearing about this protocol? If it
is the former, then I think the "protocol" is way underspecified.
Capy serves the three audiences described above.
3. I can write my own coroutine return type that has two-argument await_suspend, but lacks await_ready and await_resume. It will pass the IoAwaitable check, but I cannot use it inside a coroutine with capy::task as a return type. Apparently, there are more syntactic requirements here.
The IoAwaitable documentation states:
"An awaitable satisfies IoAwaitable if..."
An IoAwaitable must be an awaitable, which requires at a minimum the three members described.
Term "awaitable" is never defined. Note that with the await_transform magic in place even type `int` can be co_awaited on in some coroutines.
4. And IoAwaitable doesn't mention any semantic requirements. Concepts without semantic requirements are not concepts.
https://develop.capy.cpp.al/capy/reference/boost/capy/IoAwaitable.html#_sema...
My bad.
5. Is it enough for my type to satisfy the concept or is there more that I have to do? How do I make `co_await this_coro::environment` work? Is it even part of the "IoAwaitable protocol"? How is the environment propagated down to other people's coroutines (with their own return types) unless I manually make it work?
"co_await this_coro::environment" is a feature of the task type. It has nothing to do with the IoAwaitable protocol, hence it is silent on it.
So, my type somehow automatically passes the environment down, but my coroutines cannot access it? So how can my coroutines respond to a stop request?
Environments are propagated automatically to any awaitable which implements the IoAwaitable protocol, that's the point of it.
I can see how the environment is propagated *from* Capy types to my types. But I cannot see how my types propagate the environment automatically to other types.
6. What happens with my promise_type's initial_suspend and final_suspend? Are they invoked or overridden by capy::tasks's await_transform?
I believe initial_suspend and final_suspend are invoked directly by the compiler and are not affected (Klemens?)
If so, can I make my coroutines not lazy? That is, not suspend in the initial_suspend point? Regards, &rzej;
I hope that questions demonstrate how unclear it is to the reader what "IoAwaitable protocol" is.
Well... :)
Thanks
On Sun, Jun 28, 2026 at 12:00 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
Term "awaitable" is never defined
Hmm... you are right! I thought awaitable was a C++ concept. It seems it is not. There is no formal name for the awaitable/awaiter (Confidence: medium, I googled it). Anyway "awaitable" in this context means the type with await_ready, await_suspend, await_resume, required in [expr.await].
So, my type somehow automatically passes the environment down, but my coroutines cannot access it? So how can my coroutines respond to a stop request?
If you are using capy::task then the docs explain how to get the stop token. If you are writing your own task type, you fall in the category of "framework author", and then you have to design a mechanism to get the stop token.
I can see how the environment is propagated *from* Capy types to my types. But I cannot see how my types propagate the environment automatically to other types.
Hmm... yes, you are right. P4003 explains it but very briefly "the caller's await_transform injects the environment as a pointer parameter." This is not sufficient. If so, can I make my coroutines not lazy? That is, not suspend in the
initial_suspend point?
Yes and that's how synchronous I/O works. Thanks
niedz., 28 cze 2026 o 21:51 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
On Sun, Jun 28, 2026 at 12:00 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
Term "awaitable" is never defined
Hmm... you are right! I thought awaitable was a C++ concept. It seems it is not. There is no formal name for the awaitable/awaiter (Confidence: medium, I googled it).
Anyway "awaitable" in this context means the type with await_ready, await_suspend, await_resume, required in [expr.await].
So, my type somehow automatically passes the environment down, but my coroutines cannot access it? So how can my coroutines respond to a stop request?
If you are using capy::task then the docs explain how to get the stop token.
If you are writing your own task type, you fall in the category of "framework author", and then you have to design a mechanism to get the stop token.
I can see how the environment is propagated *from* Capy types to my types. But I cannot see how my types propagate the environment automatically to other types.
Hmm... yes, you are right. P4003 explains it but very briefly "the caller's await_transform injects the environment as a pointer parameter." This is not sufficient.
If so, can I make my coroutines not lazy? That is, not suspend in the
initial_suspend point?
Yes and that's how synchronous I/O works.
But this seems to clash with how the environment is propagated. It is my understanding that the stop token and the executor are propagated to the user coroutine's promise upon the first suspension. So, in my eager coroutine, if I want to respond to the stop request, I do not have the stop token yet. Or am I missing something? Regards, &rzej;
Thanks
On 6/28/26 20:25, Andrzej Krzemienski via Boost wrote:
4. And IoAwaitable doesn't mention any semantic requirements. Concepts without semantic requirements are not concepts.
Actually, from the perspective of the C++ standard the opposite is true. Concepts are introduced with the 'concept' keyword and defined through 'requires' clauses, and any type that matches these 'requires' clauses implements the concept as far as the language is concerned, regardless of semantics. Things like AssociativeContainer, which define semantic requirements beyond what can be specified through 'requires' clauses, are called "named requirements" instead of "concepts", Yes, this goes against a long history of using the word "concept" in its generic English sense and calling named requirements concepts. But now that the concept keyword is a thing, I find it less confusing to restrict the word "concept" to the entities introduced by that keyword. -- Rainer Deyke - rainerd@eldwood.com
El 29/06/2026 a las 9:07, Rainer Deyke via Boost escribió:
On 6/28/26 20:25, Andrzej Krzemienski via Boost wrote:
4. And IoAwaitable doesn't mention any semantic requirements. Concepts without semantic requirements are not concepts.
Actually, from the perspective of the C++ standard the opposite is true. Concepts are introduced with the 'concept' keyword and defined through 'requires' clauses, and any type that matches these 'requires' clauses implements the concept as far as the language is concerned, regardless of semantics. Things like AssociativeContainer, which define semantic requirements beyond what can be specified through 'requires' clauses, are called "named requirements" instead of "concepts",
Yes, this goes against a long history of using the word "concept" in its generic English sense and calling named requirements concepts. But now that the concept keyword is a thing, I find it less confusing to restrict the word "concept" to the entities introduced by that keyword.
Actually, C++20 concepts may (and often do) come with semantic requirements, as explicitly acknowledged at https://eel.is/c%2B%2Bdraft/constraints?utm_source=chatgpt.com#res.on.requir... The rationale is that, even though the compiler can't statically check satisfaction of semantic requirements, these requirements are assumed to hold. Joaquín M López Muñoz
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 - rainerd@eldwood.com
On Monday, June 29th, 2026 at 12:24 PM, Rainer Deyke via Boost <boost@lists.boost.org> wrote:
+-------------------+ | 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.
I think this is a valid criticism. It's designed with I/O in mind but it is certainly not limited to it and the name should reflect that.
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 agree on the behavior as it's the purpose of the IoAwaitable protocol. But in order to fulfill that contract, we are back to two executor hops the same as run() does today. Both run() and resume_on() necessarily perform 2 executor hops. With regard to the frames, to eliminate them you introduce branching into every task's final_suspend which is not ideal. Or you introduce a trampoline and you introduce a frame. The frames you are saving in this case are handled by a bump allocator and likely not calling malloc. Because of that, I don't think the savings are as significant. We definitely aim to deliver an ergonomic API. Is there a case where the top-layer run() doesn't collapse the nesting? Perhaps the only advantage ergonomically is that you don't have to capture local variables that exist in the coroutine frame.
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.
As written I don't think it's viable. Calling resume() from post adds stack frames and introduces stack growth, which we deliberately avoid. The inline behavior already exists. When an executor is safe to run inline (already on the right thread), its dispatch returns the continuation handle for symmetric transfer. This bypasses scheduling, executes immediately, and adds no stack frame. That's the sanctioned fast path and basically every executor implements it that way. With that in mind, is this necessary at all?
On 6/29/26 15:27, Steve Gerbino via Boost wrote:
On Monday, June 29th, 2026 at 12:24 PM, Rainer Deyke via Boost <boost@lists.boost.org> wrote:
+-------------------+ | 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.
I think this is a valid criticism. It's designed with I/O in mind but it is certainly not limited to it and the name should reflect that.
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 agree on the behavior as it's the purpose of the IoAwaitable protocol. But in order to fulfill that contract, we are back to two executor hops the same as run() does today. Both run() and resume_on() necessarily perform 2 executor hops.
It's easy to come up with examples where executor hops are saved by resume_on. task<int> f() { capy::run(ex1)([] { do_something_on_e1(); }); capy::run(ex2)([] { do_something_on_e2(); }); capy::run(ex3)([] { do_something_on_e3(); }); } task<void> f(int a) { capy::resume_on(ex1); do_something_on_ex1(); capy::resume_on(ex2); do_something_on_ex2(); capy::resume_on(ex3); do_something_on_ex3(); } That's 6 hops (2N) for capy::run, 4 hops (N+1) for on_resume.
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.
As written I don't think it's viable. Calling resume() from post adds stack frames and introduces stack growth, which we deliberately avoid.
Yes, there's a risk of stack overflow. It can be mitigated by returning the coroutine handle from dispatch instead of calling resume, but it can't be eliminated entirely. This is definitely not a very safe, or otherwise very good, general purpose executor. It's an escape hatch for high performance code where the user really knows what he's doing.
The inline behavior already exists. When an executor is safe to run inline (already on the right thread), its dispatch returns the continuation handle for symmetric transfer.
As you said, only in contexts where you're already on the right thread. Consider this example again: task<int> f() { capy::run(ex1)([] { do_something_on_e1(); }); capy::run(ex2)([] { do_something_on_e2(); }); capy::run(ex3)([] { do_something_on_e3(); }); } Running this on an immediate_executor effectively eliminates three of the six executor hops, and the amount of time it spends on the wrong thread is negligible. -- Rainer Deyke - rainerd@eldwood.com
On Mon, Jun 29, 2026 at 8:02 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
It's easy to come up with examples where executor hops are saved by resume_on.
task<int> f() { capy::run(ex1)([] { do_something_on_e1(); }); capy::run(ex2)([] { do_something_on_e2(); }); capy::run(ex3)([] { do_something_on_e3(); }); }
Yes of course this is a contrived example but what is the use case? What executors are these? Does each represent a different thread pool? A different pinned CPU? Why are you launching a coroutine on one executor, and then sequentially waiting for each of three coroutines, each launched on a different executor? Thanks
On Mon, Jun 29, 2026 at 3:25 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
This is my second formal review of Capy, meant to replace the original, which I am hereby formally withdrawing.
Thank you for refocusing on the core concepts, Rainer. You're right that the core of Capy is the protocol for coroutine environment propagation, and that's the right lens for evaluation. I'll respond to each section.
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.
Yes and there's a little more to it. The name is political. "I/O" is an unclaimed domain in the standards committee and one where we have evidence that it is the most natural fit for coroutines. I do recognize that the name is imperfect. However, and I will refer back to P4172, there are the three audiences: 1. Application authors 2. Framework authors 3. I/O library authors Only groups 2 and 3 are exposed to the concept name, and together this cohort is vastly smaller. This cohort is also more skilled and specialized. The name is in theory less consequential. Regardless, we are considering "Cowaitable" yet of course that has its own problems. A rename would be ideally timed before any initial Boost release in which the library appears (assuming it is accepted).
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.
The "bouncing" framing doesn't match how the library is used in practice. In the primary use case, (networking) you launch a coroutine chain on an executor (a strand, an io_context) and the entire chain stays there. No executor hopping. The executor affinity is the point: you set it once and stop thinking about it. The cost you describe only appears when you explicitly choose to switch executors via `run`. That's a deliberate action, not something that happens behind your back. The question I'd ask is: why would you be switching executors frequently enough for the overhead to matter? In I/O code, executor switches are rare. You launch on a strand or an io_context, and you stay there.
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.
I appreciate the thoughtfulness to approaches. I'll address each escape hatch.
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.
The extra coroutine frame exists because `run` creates a new `io_env` for the child: new executor, inherited stop token and allocator. The trampoline ensures you return to your original executor when the child completes. This is the price of correctness. There are three problems with `resume_on`: 1. the library provides the tools for anyone to implement `resume_on` themselves. The executor concept, `continuation`, `dispatch`, and `post` are all public. Nothing prevents a user from building `resume_on` as a standalone awaitable. We don't have to ship it to enable it. 2. `resume_on` breaks the environment model. When you `resume_on(ex)` and then `co_await` an IoAwaitable, the `io_env` still points to the *original* executor. The IoAwaitable dispatches its completion to the wrong executor. To fix this, you either need to update the `io_env` (but it's `const*` and shared - who owns the new one?) or allocate a new `io_env` (which is exactly what `run` already does). So `resume_on` either breaks the environment or duplicates the machinery of `run`, minus the safety of scoped lifetime. 3. coroutines are both easy to use and hard to use. They're more ergonomic than callbacks, but the C++ committee standardized the machinery in 2020 and then spent six years building P2300 senders instead of providing library components. The consequence is that the ecosystem has minimal collective experience with coroutines. Providing `resume_on` as a first-class API in a Boost library would invite misuse from users who are still learning the fundamentals. It's an expert feature disguised as a convenience, with significant sharp edges. There's already enough sharp edges in Capy.
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.
This caveat is exactly what makes `resume_on` as complex as `run`. When coroutine B calls `resume_on(ex2)` and then `co_return`s, coroutine A must resume on A's original executor. That means `final_suspend` must know which executor the parent was on and dispatch back to it. That is exactly what the trampoline in `run` does. The "simpler" `resume_on` requires the same machinery as `run` to be correct. The complexity is merely hidden not removed. Implementing this scoping correctly is enormously complex. I'm not even certain it's feasible without the trampoline mechanism that `run` already provides. `run` IS `resume_on`, implemented correctly, with scoped lifetime and automatic return-to-caller. The extra coroutine frame is the price of correctness. The existing design rationale is documented in the "Capy and TooManyCooks" comparison (Section 8 of the documentation). We'll expand that document to address `resume_on` specifically and explain why `run` is the correct implementation of the same idea.
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... };
How do you propose not overflowing the stack? Your `post` calls `c.h.resume()` directly. That resumes a coroutine from inside another coroutine's execution context. Each `resume()` adds a stack frame. This is the exact problem symmetric transfer was invented to solve. `await_suspend` returns a `coroutine_handle<>` so the runtime can tail-call it without growing the stack. The `dispatch` path can inline via symmetric transfer (returning `c.h` from `await_suspend`), but `post` cannot - it's called from contexts where there is no `await_suspend` return value to tail-call through. An immediate_executor that calls `resume()` directly from `post` defeats the entire mechanism. Beyond stack overflow, there are further constraints. `async_mutex` stop callbacks call `executor.post(cont_)` from arbitrary threads. An immediate_executor that inlines `post` would resume the coroutine on the wrong thread, corrupting the thread-local frame allocator. This is, ironically, exactly the class of bug you asked us to document in condition 4.
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.
We wouldn't say that, and I'm glad you raised it. Educating users is not just Capy's business, it's our obligation. This follows the tradition I started with Beast, which included extensive conceptual documentation precisely because the domain was new to most C++ developers. Coroutines are in the same position today - arguably worse. The committee standardized coroutine machinery in C++20 and then provided no library components for six years. This wasn't an accident. P2300 section 1.9.2 dismissed coroutines as a basis for asynchrony with five paragraphs of text and no supporting measurements. "Symmetric transfer" - the mechanism that makes coroutines safe and efficient - does not appear once in eleven revisions of that paper. The consequence: no standard coroutine library components, no educational infrastructure, no ecosystem experience. That gap is why reviewers ask about resume_on and immediate_executor - not because the ideas are wrong, but because the fundamentals haven't been taught. Users don't just lack experience with Capy; they lack experience with coroutines in general. Where else will they go? We are the experts. Who else but us should write the tutorial? Who else could? Capy is filling a hole the committee dug. Many of the concerns raised in this review: executor switching, immediate resumption, runtime cost - trace back to that knowledge gap. We need to explain the *why*, not just the *how*. The primer should cover pitfalls beyond the obvious: - Dangling references in lambda captures - Constructing a task without immediately co_awaiting it - the task captures references to the caller's stack frame at construction time. Store it and co_await later, those references dangle. You don't need a lambda for this to bite you. - Why `std::mutex` inside a coroutine running on a thread pool is a deadlock waiting to happen - Why you must never call `h.resume()` from a stop_callback - post through the executor instead - Why symmetric transfer exists - calling `resume()` directly grows the stack, which is why executors queue work instead of resuming inline Your original review is excellent source material for gotchas encountered by a knowledgeable reviewer. We'll reference it.
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. [...] 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?
They serve different granularity and should not be merged. We'll add a comparison section to the documentation. The key distinctions: A strand is coarse-grained. All work dispatched through the strand serializes. Use it when an entire coroutine tree needs exclusive access - the typical case is per-connection state in a server. You launch the connection handler on a strand and everything downstream serializes automatically. An async_mutex is fine-grained. Coroutines run concurrently and only serialize at lock/unlock points. Use it when most of your coroutine's work can run in parallel and only specific shared variables need protection. A strand has no failure mode for acquisition - you're either on it or not. An async_mutex returns `io_result<>` because lock acquisition can be canceled via stop token. A strand supports direct posting (`strand.post(c)`) without `co_await`. An async_mutex always requires `co_await` because it may need to suspend. They are often used together. The async_mutex example in the documentation runs on a strand to satisfy the mutex's single-executor threading requirement.
Apparently locking can fail for mutexes but not for strands? Or do strands just report failure differently?
Locking an async_mutex can fail because lock acquisition is cancellable. If the coroutine's stop token fires while it's waiting in the queue, the lock attempt completes with `error::canceled`. This is a feature, not an asymmetry - a strand doesn't "lock" in the same sense. You dispatch work to it, and it runs when the strand is available. There's no queue-entry cancellation because there's no explicit acquisition step.
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.
Agreed. The documentation example uses `std::coroutine_handle<>` where it should use `continuation`. The header comment in `io_awaitable.hpp` has the correct example. We will synchronize the doc with the header.
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.
To summarize: 1. IoAwaitable documentation: will fix. 2. resume_on - `run` is resume_on, implemented correctly. The extra frame is the price of scoped lifetime and automatic return-to- caller. We'll expand the design rationale documentation. 3. immediate_executor - stack overflow, wrong-thread resumption, and frame allocator corruption prevent a safe general implementation. We'll document the constraints for expert users. 4. Coroutine safety documentation: yes! Educating users is our responsibility and we'll expand the primer substantially. 5. Strand vs async_mutex - accepted. We'll add comparison documentation with usage guidelines. Vinnie
participants (6)
-
Andrzej Krzemienski -
Joaquin M López Muñoz -
Peter Dimov -
Rainer Deyke -
Steve Gerbino -
Vinnie Falco