Code firstly, very simple example code:
template <typename CompletionToken>
decltype(auto) test_initiate(CompletionToken&& token) {
return boost::asio::async_initiate<CompletionToken, void(void)>([](auto&& handler) mutable { std::move(handler)(); },
token);
}
struct compose_tester {
template <typename Self>
void operator()(Self& self) {
self.complete();
}
};
template <typename CompletionToken>
decltype(auto) test_compose(CompletionToken&& token) {
return boost::asio::async_compose<CompletionToken, void(void)>(compose_tester{}, token);
}
The example code does nothing but call the completion handler. everything seems good right now.
But what if I call the example methods with bind_executor
?
boost::asio::io_context ioc1;
boost::asio::io_context ioc2;
std::thread::id t0_id = std::this_thread::get_id();
std::thread::id t1_id;
std::thread::id t2_id;
// Hide the code that runs ioc1 & ioc2 in two threads and sets t1_id & t2_id. Proper work_guard objects have been created.
test_initiate(boost::asio::bind_executor(ioc2, [&]() {
std::cerr << "Should run @2:" << (std::this_thread::get_id() == t2_id ? "true" : "false") << std::endl;
std::cerr << "Should not run @0:" << (std::this_thread::get_id() == t0_id ? "true" : "false") << std::endl;
}));
test_compose(boost::asio::bind_executor(ioc2, [&]() {
std::cerr << "Should run @2:" << (std::this_thread::get_id() == t2_id ? "true" : "false") << std::endl;
std::cerr << "Should not run @0:" << (std::this_thread::get_id() == t0_id ? "true" : "false") << std::endl;
}));
It clearly shows that both callbacks run in the main thread (aka. thread id equals to t0_id), but this isn't the desired behaviour.
After digging into the source code of asio, I found both async_initiate and async_compose merely forward the arguments to the handler. The correct way to invoke the handler is to wrap it with dispatch
:
template <typename CompletionToken>
decltype(auto) test_initiate2(CompletionToken&& token) {
return boost::asio::async_initiate<CompletionToken, void(void)>(
[](auto&& handler) mutable { boost::asio::dispatch(std::move(handler)); }, token);
}
struct compose_tester2 {
template <typename Self>
void operator()(Self& self) {
boost::asio::dispatch(boost::asio::get_associated_executor(self),
[self = std::move(self)]() mutable { self.complete(); });
}
};
template <typename CompletionToken>
decltype(auto) test_compose2(CompletionToken&& token) {
return boost::asio::async_compose<CompletionToken, void(void)>(compose_tester2{}, token);
}
Now the example above works correctly.
My questions are:
dispatch
internally?Yes, almost. When using get_associated_executor
always provide a sensible fallback. That is because otherwise you risk getting a system_executor
back which can silently introduce UB into your application.
I don't purport to have the full answer here. But some arguments could be:
asio::defer
, asio::post
or asio::dispatch
; if it were "baked into the library helpers" that distinction would vanish