You want to associate your executor with the completion token, and then let post/dispatch/defer figure it out from there:
co_await asio::post(bind_executor(pstrand, asio::use_awaitable));
See also e.g. When must you pass io_context to boost::asio::spawn? (C++) or boost::asio::bind_executor does not execute in strand for more details on how it works/why it works/when it works.
It does in fact explain how
get_associated_executor
does use a default so it might explain how explicitly posting to an executor didn’t seem to work here (I haven’t checked the associated executor implementation foruse_awaitable
just now)
Here’s my take:
#include <boost/asio.hpp>
#include <boost/asio/experimental/as_single.hpp>
#include <boost/bind/bind.hpp>
#include <coroutine>
#include <iostream>
#include <thread>
inline void tout(auto const& msg) {
static std::mutex mx;
std::lock_guard lk(mx);
static const std::hash<std::thread::id> h{};
std::cout << "T" << (h(std::this_thread::get_id()) % 100) << " " << msg
<< std::endl;
}
namespace asio = boost::asio;
asio::awaitable<void> mainCo(asio::io_context& appIO,
asio::io_context& prodIO) {
auto to_app = bind_executor(make_strand(appIO), asio::use_awaitable);
auto to_prod = bind_executor(make_strand(prodIO), asio::use_awaitable);
tout("MC on APPIO");
co_await asio::post(to_prod); tout("MC on PRODIO");
co_await asio::post(to_app); tout("MC on APPIO");
co_await asio::post(to_prod); tout("MC on PRODIO");
co_await asio::post(to_prod); tout("MC on PRODIO");
co_await asio::post(to_app); tout("MC on APPIO");
}
int main() {
asio::io_context prodIO, appIO;
auto prodWork = asio::make_work_guard(prodIO);
std::thread prodThread{[&prodIO] {
tout("ProdThread run start");
prodIO.run(); // if this call is removed the mainCo is stuck as
// expected
tout("ProdThread run done");
}};
asio::co_spawn(appIO, mainCo(appIO, prodIO), asio::detached);
tout("MainThread run start");
appIO.run();
tout("MainThread run done");
prodWork.reset();
prodThread.join();
}
Prints e.g.
T49 ProdThread run start
T31 MainThread run start
T31 MC on APPIO
T49 MC on PRODIO
T31 MC on APPIO
T49 MC on PRODIO
T49 MC on PRODIO
T31 MC on APPIO
T31 MainThread run done
T49 ProdThread run done
BONUS
I’d suggest passing executors, not execution context references. It’s cleaner and more flexible: https://godbolt.org/z/Tr54vf8PM
It then makes it trivial to replace the prodIO + thread
with a simple thread_pool
execution context. It removes the need for work guards and also the fixes missing exception handling: https://godbolt.org/z/a3GT61qdh