Skip to content

Comments

Fix async handlers running on wrong executor#3075

Open
ssam18 wants to merge 1 commit intoboostorg:developfrom
ssam18:fix-async-base-executor-binding
Open

Fix async handlers running on wrong executor#3075
ssam18 wants to merge 1 commit intoboostorg:developfrom
ssam18:fix-async-base-executor-binding

Conversation

@ssam18
Copy link

@ssam18 ssam18 commented Jan 24, 2026

The async_base::complete function now correctly binds the dispatched handler to the operation's executor, thereby ensuring the appropriate execution context.

When async_base::complete dispatches a handler, it now properly binds
the handler to the operation's executor to ensure correct execution context.
@sehe
Copy link
Contributor

sehe commented Jan 24, 2026

The fix (also?) doesn't look accurate to me.

In particular, you applied it to the branch where the documentation claims the immediate executor should be used, however, your bind overrides it with any associated non-immediate executor.

Note that this->get_immediate_executor() correctly invokes boost::asio::get_associated_immediate_executor on the underlying handler h_.

Assuming that you ran into this as a problem in your code, can you share a minimal example where the wrong executor is used? Then we can understand what actually needs to be fixed. I can see two potential causes here:

That is, assuming you have demonstrated a case where the wrong executor is effectively used.

@ashtum
Copy link
Collaborator

ashtum commented Feb 1, 2026

I believe this is related to the issue briefly described in #3047.

Asio recommends using async_immediate inside composed operations to ensure the correct semantics for immediate completion. For example:

asio::async_immediate(self.get_io_executor(), std::move(self));

Internally, this leads to a call to dispatch that looks like:

template <typename CompletionHandler>
void operator()(CompletionHandler&& handler) const
{
  typename associated_immediate_executor<
    CompletionHandler, executor_type>::type ex =
      (get_associated_immediate_executor)(handler, ex_);
  (dispatch)(ex, static_cast<CompletionHandler&&>(handler));
}

If the handler has an associated executor, dispatch wraps it in asio::detail::work_dispatcher, which then dispatches the handler on that associated executor.

In this context, the handler is the composed operation object (self). When the supplied completion_token does not have an associated executor, self falls back to using the stream’s executor. As a result, asio::detail::work_dispatcher is always involved, and the completion handler is guaranteed to run either on its associated executor or on the stream’s executor.

Our current implementation behaves slightly differently. Instead of passing the composed operation object to dispatch, we pass the final handler (h_) directly:

auto const ex = this->get_immediate_executor();
net::dispatch(
    ex,
    net::append(std::move(h_), std::forward<Args>(args)...));

Because of this, dispatch does not attempt to use asio::detail::work_dispatcher with the stream’s executor when the handler lacks an associated executor. Consequently, the earlier guarantee no longer holds:

and the completion handler is guaranteed to run either on its associated executor or on the stream’s executor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants