While experimenting with boost::asio::awaitable
and Executor
s, I keep observing some rather confusing behaviour that I would like to understand better
Preparation
Please take a look at the follwing program:
int main(int argc, char const* args[])
{
boost::asio::io_context ioc;
auto spawn_strand = boost::asio::make_strand(ioc);
boost::asio::co_spawn(spawn_strand,
[&]() -> boost::asio::awaitable<void>
{
auto switch_strand = boost::asio::make_strand(ioc);
co_await boost::asio::post(switch_strand, // (*)
boost::asio::bind_executor(switch_strand, boost::asio::use_awaitable));
boost::asio::post(spawn_strand, [](){
std::cout << "calling handler\n";
});
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "waking up\n";
},
boost::asio::detached);
std::jthread threads[3]; // provide enough threads to serve strands in parallel
for (auto& thread : threads)
thread = std::jthread{ [&]() { ioc.run(); }};
}
As expected, this program outputs:
calling handler
waking up
Specifically, "calling handler"
is printed before "waking up"
since, after line (*)
, the coroutine no longer runs on spawn_strand
. Thus, latter will not get blocked by sleep_for
and can run the handler immediately.
So far so good, now let's reveal some confusing behaviour...
Observations
It turns out that
assert
ingspawn_strand == co_await boost::asio::this_coro::executor
after line(*)
does not fail. We would assume that, at this point, the execution switched toswitch_strand
and actually just confirmed this fact. Therefore, at this point I'd expectco_await boost::asio::this_coro::executor
to compare equal toswitch_strand
instead ofspawn_strand
.If in line
(*)
we replace the strand passed topost
withspawn_strand
, the output changes to:waking up calling handler
I've read other answers (e.g. this or this) regarding the executors supplied to
post
andbind_executor
and generally they suggest that the former serves as a mere fallback in case the handler does not have an associated handler. This does not match with the observed output.Now, let's take the change of the former paragraph and add
boost::asio::post(switch_strand, [](){ std::cout << "calling handler\n"; });
after line
(*)
. Note that this time we useswitch_strand
instead ofspawn_strand
. We will get the following outputwaking up calling handler calling handler
This suggest that both handlers (on
switch_strand
as well as onspawn_strand
), are blocked by the singlesleep_for
. Ultimately it seems like after line(*)
the coroutine is run on both strands at the same time, which it very irritating.Bullet point 2. and 3. equally apply when, instead of replacing the stand passed to
post
, we replace the strand passed tobind_handler
byspawn_strand
.
Question
How can these strange observations be explained? To me it seem like, in addition to the usual functioning of executors to invoke handlers, boost::asio::awaitable
s are associated with an additional executor (namely the one provided to co_spawn
and accessible via this_coro::executor
) permanently present during execution of the coroutine. However, I haven't found any of this explained anywhere; neither the documentation for Boost.Asio C++20 Coroutines Support nor in answers to related questions here. So I don't believe this is how things actually work.