While experimenting with boost::asio::awaitable and Executors, 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
assertingspawn_strand == co_await boost::asio::this_coro::executorafter line(*)does not fail. We would assume that, at this point, the execution switched toswitch_strandand actually just confirmed this fact. Therefore, at this point I'd expectco_await boost::asio::this_coro::executorto compare equal toswitch_strandinstead ofspawn_strand.If in line
(*)we replace the strand passed topostwithspawn_strand, the output changes to:waking up calling handlerI've read other answers (e.g. this or this) regarding the executors supplied to
postandbind_executorand 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_strandinstead ofspawn_strand. We will get the following outputwaking up calling handler calling handlerThis suggest that both handlers (on
switch_strandas 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_handlerbyspawn_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::awaitables 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.