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