c++boostboost-asio

Why doesn't boost::asio::async_initiate or async_compose automatically call the handler via its bound executor


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:

  1. Am I doing this correctly?
  2. Why doesn't asio wrap the handler with dispatch internally?

Solution

    1. Am I doing this correctly?

    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.

    1. Why doesn't asio wrap the handler with dispatch internally?

    I don't purport to have the full answer here. But some arguments could be: