1

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?

1 Answer 1

2
  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:

  • you can choose whether to asio::defer, asio::post or asio::dispatch; if it were "baked into the library helpers" that distinction would vanish
  • there might be cases where you trigger immediate completion which might use a different executor
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the reply. I tried to implement this feature by myself and it turned out to be extremely difficult. Making the handler invoked by boost::dispatch / post / defer is easy, but when it comes to compatibility with assoicate_executor / assoicate_cancellation_slot stuff... these types must be forwarded to the wrapper class as-if the executor / cancellation slot were bound to it directly. This introduces many tricks rely on boost::asio::detail::...Even though I've completed and tested the code, I wonder if it's worth risking such possible bug-prone code, just to save a few lines of code.
@Simon.Li I always defer to the library designer. It's my experience that they have a good sense for the balance. In fact, things have gotten much more friendly over the releases. E.g. there's boost::asio::any_completion_handler which does all the mechanics, and also it's possible to "delegate" all associators generically these days (see e.g. the ~8 LoC under "Loose Ends" in a recent answer). But I hear you. I don't have a ready fire-proof explanation why the design choice is as it is. I'm content to follow Chris Kohlhoff
After taking some time to learn the boost::beast library, I found a better way to propagate the associated executor, cancellation slot, etc. Implement a specialization of boost::asio::associator; example code is located in the file "boost/beast/core/detail/bind_handler.hpp". Although I think boost::asio deserves more documentation — the use of templates is more sophisticated than anything I've ever seen, but it has extremely high educational value.
Yes that's exactly what I showed in the linked answer under Loose Ends. The corresponding documentation would be here: boost.org/doc/libs/1_88_0/doc/html/boost_asio/overview/model/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.