C++
Boost.Asio
io_context
concurrency
asynchronous-programming

Which io_context does stdboostasiopost / dispatch use?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When using Boost.Asio for asynchronous programming in C++, boost::asio::post and boost::asio::dispatch are the primary functions for scheduling work onto an executor. A common question is: which io_context do these functions use? The answer depends on how you call them and what executor is associated with the handler. This article breaks down the rules governing io_context selection for both post and dispatch, with code examples that demonstrate each scenario.

What Is io_context?

The io_context (called io_service in older versions of Boost.Asio) is the core event loop in Boost.Asio. It manages a queue of pending handlers and executes them when you call io_context::run(). Every asynchronous operation in Boost.Asio ultimately delivers its completion handler through an io_context.

cpp
1#include <boost/asio.hpp>
2#include <iostream>
3
4int main() {
5    boost::asio::io_context io;
6
7    boost::asio::post(io, []() {
8        std::cout << "Handler executed on io_context" << std::endl;
9    });
10
11    io.run();  // Processes queued handlers
12    return 0;
13}

In this example, post explicitly receives io as the first argument, so there is no ambiguity. The handler runs on that specific io_context.

How post Selects the io_context

boost::asio::post has two overloads that determine which io_context is used.

Overload 1: Explicit Executor or io_context

When you pass an executor (or io_context) as the first argument, the handler is queued on that executor. This is the most straightforward case.

cpp
1boost::asio::io_context io1;
2boost::asio::io_context io2;
3
4boost::asio::post(io1, []() {
5    std::cout << "Runs on io1" << std::endl;
6});
7
8boost::asio::post(io2, []() {
9    std::cout << "Runs on io2" << std::endl;
10});
11
12// Run both
13std::thread t1([&io1]() { io1.run(); });
14std::thread t2([&io2]() { io2.run(); });
15t1.join();
16t2.join();

Each handler runs on the io_context that was explicitly provided.

Overload 2: Handler with Associated Executor

When you call post with only a handler (no explicit executor), Boost.Asio inspects the handler for an associated executor. The associated executor is determined through the boost::asio::associated_executor trait. If the handler was bound to an executor using boost::asio::bind_executor, that bound executor is used.

cpp
1boost::asio::io_context io;
2auto executor = io.get_executor();
3
4auto bound_handler = boost::asio::bind_executor(executor, []() {
5    std::cout << "Runs on the bound executor's io_context" << std::endl;
6});
7
8boost::asio::post(bound_handler);
9io.run();

Here, even though post receives no explicit io_context, the handler carries its executor association, and post uses that.

If the handler has no associated executor and you call post without an explicit executor, Boost.Asio uses the system executor (boost::asio::system_executor), which runs the handler on a system-managed thread.

How dispatch Differs from post

The key difference between dispatch and post is that dispatch may execute the handler immediately if certain conditions are met, while post always defers execution.

post always enqueues the handler for later execution. It never runs the handler inline. This guarantees that the handler executes outside the calling context.

dispatch checks whether the caller is already running inside the target io_context. If so, it executes the handler immediately (inline). If not, it behaves like post and enqueues the handler.

cpp
1boost::asio::io_context io;
2
3boost::asio::post(io, [&io]() {
4    std::cout << "Outer handler" << std::endl;
5
6    // dispatch: runs immediately because we are already inside io.run()
7    boost::asio::dispatch(io, []() {
8        std::cout << "Inner dispatch: runs inline" << std::endl;
9    });
10
11    // post: always deferred, runs after outer handler completes
12    boost::asio::post(io, []() {
13        std::cout << "Inner post: runs after outer completes" << std::endl;
14    });
15});
16
17io.run();

Output:

 
Outer handler
Inner dispatch: runs inline
Inner post: runs after outer completes

The dispatch call executes its handler immediately because the code is already running inside io.run(). The post call enqueues its handler, so it runs after the current handler finishes.

When to Use Each

Use post when you want to guarantee that the handler runs asynchronously, outside the current call stack. This is important for avoiding reentrancy issues. For example, if a handler modifies shared state and another handler reads it, post ensures they do not run simultaneously on the same thread.

Use dispatch when you want the handler to run as soon as possible. If you are already inside the right execution context, dispatch avoids the overhead of queuing and dequeuing. This is useful for performance-sensitive paths where the handler is lightweight.

cpp
1// Safe pattern: always defer to avoid reentrancy
2void safe_notify(boost::asio::io_context& io, std::function<void()> handler) {
3    boost::asio::post(io, std::move(handler));
4}
5
6// Performance pattern: run inline if possible
7void fast_notify(boost::asio::io_context& io, std::function<void()> handler) {
8    boost::asio::dispatch(io, std::move(handler));
9}

Strand-Based Execution

When using boost::asio::strand for serialized execution, post and dispatch respect the strand's ordering guarantees. A strand ensures that handlers posted to it never execute concurrently, even across multiple threads.

cpp
1boost::asio::io_context io;
2boost::asio::strand<boost::asio::io_context::executor_type> strand(io.get_executor());
3
4boost::asio::post(strand, []() {
5    std::cout << "First on strand" << std::endl;
6});
7
8boost::asio::post(strand, []() {
9    std::cout << "Second on strand" << std::endl;
10});
11
12// Run with multiple threads
13std::vector<std::thread> threads;
14for (int i = 0; i < 4; ++i) {
15    threads.emplace_back([&io]() { io.run(); });
16}
17for (auto& t : threads) t.join();

The strand guarantees that "First" prints before "Second," regardless of how many threads call io.run().

Common Pitfalls

  1. Assuming dispatch is always deferred. Unlike post, dispatch can run the handler synchronously. If your handler modifies data that the caller is also using, this inline execution can cause subtle bugs. Use post when you need guaranteed deferral.
  2. Forgetting to call io_context::run(). Handlers queued with post or dispatch will not execute until some thread calls run() on the associated io_context. If run() is not called, the program appears to hang or silently drops work.
  3. Mixing io_context instances unintentionally. If your application has multiple io_context objects, make sure each handler is posted to the correct one. A handler posted to io1 will never execute if only io2.run() is called.
  4. Not using strands for shared state. Without a strand, handlers on the same io_context can run concurrently across multiple threads. If those handlers access shared data, you need either a strand or explicit locking.

Summary

boost::asio::post and dispatch use the io_context you provide explicitly as the first argument. If no executor is provided, they fall back to the handler's associated executor (set via bind_executor) or the system executor. The key difference is that post always defers execution, while dispatch runs the handler inline if the caller is already inside the target executor's context. Use post for safety and dispatch for performance when reentrancy is not a concern.


Course illustration
Course illustration

All Rights Reserved.