Executors
Understand how executors control where and how operations run.
What is an Executor?
An executor represents a context where work can be executed. It answers:
-
Where does work run? (Which thread pool? Which io_context?)
-
How is work submitted? (Queued? Immediate? Deferred?)
Every I/O object in Asio is associated with an executor, and completions are delivered through that executor.
Getting Executors
#include <boost/asio.hpp>
namespace asio = boost::asio;
// From io_context
asio::io_context ctx;
auto exec = ctx.get_executor();
// From an I/O object
asio::ip::tcp::socket socket(ctx);
auto exec2 = socket.get_executor();
// From a coroutine
asio::awaitable<void> example()
{
auto exec = co_await asio::this_coro::executor;
}
Executor Types
io_context::executor_type
The executor for io_context. Work submitted to this executor runs when
io_context::run() is called.
asio::io_context ctx;
asio::io_context::executor_type exec = ctx.get_executor();
any_io_executor
A type-erased executor that can hold any I/O executor:
asio::any_io_executor exec = ctx.get_executor();
Useful for:
-
Function parameters that accept any executor
-
Runtime polymorphism
strand<Executor>
An executor adapter that serializes execution:
auto strand = asio::make_strand(ctx);
See Strands for details.
Posting Work
Submit work to an executor:
auto exec = ctx.get_executor();
// Post: queue for later execution
asio::post(exec, []() {
std::cout << "Posted work\n";
});
// Dispatch: run immediately if possible, otherwise post
asio::dispatch(exec, []() {
std::cout << "Dispatched work\n";
});
// Defer: like post, but may batch with other work
asio::defer(exec, []() {
std::cout << "Deferred work\n";
});
With coroutines:
asio::awaitable<void> example()
{
auto exec = co_await asio::this_coro::executor;
// Switch to this executor (no-op if already there)
co_await asio::dispatch(exec, asio::use_awaitable);
// Post and wait
co_await asio::post(exec, asio::use_awaitable);
}
Associated Executor
I/O objects and handlers have an associated executor:
// Socket's executor (from construction)
asio::ip::tcp::socket socket(ctx);
auto sock_exec = socket.get_executor();
// Timer with explicit executor
auto strand = asio::make_strand(ctx);
asio::steady_timer timer(strand); // Timer uses the strand
When an async operation completes, the completion handler runs on the associated executor.
Binding Executors
Force a handler to run on a specific executor:
auto strand = asio::make_strand(ctx);
timer.async_wait(
asio::bind_executor(strand, [](auto ec) {
// This runs on the strand, regardless of timer's executor
}));
Executor Properties
Executors have queryable properties:
// Check if the executor requires blocking (it doesn't for io_context)
auto blocking = asio::query(exec, asio::execution::blocking);
// Get the execution context
auto& ctx = asio::query(exec, asio::execution::context);
System Executor
A lightweight executor for simple cases:
asio::system_executor exec;
asio::post(exec, []() {
// Runs on a system thread pool
});
The system executor uses a global thread pool. It’s useful when you don’t
need a dedicated io_context.
Custom Executors
You can create custom executors by implementing the executor concept. A minimal executor needs:
-
execute(f)— Run a function -
Equality comparison
-
Copy/move constructors
class my_executor
{
public:
// Execute work
template<typename F>
void execute(F&& f) const
{
// Run f somehow
f();
}
// Comparison
bool operator==(const my_executor&) const noexcept { return true; }
bool operator!=(const my_executor&) const noexcept { return false; }
};
Executor in Coroutines
Inside a coroutine, get the executor with this_coro::executor:
asio::awaitable<void> example()
{
// Get current executor
auto exec = co_await asio::this_coro::executor;
// Create objects using this executor
asio::steady_timer timer(exec, std::chrono::seconds(1));
asio::ip::tcp::socket socket(exec);
// Spawn child coroutines on the same executor
asio::co_spawn(exec, child_coroutine(), asio::detached);
}
Thread Pool
Asio provides a thread pool with its own executor:
asio::thread_pool pool(4); // 4 threads
// Get executor
auto exec = pool.get_executor();
// Post work
asio::post(exec, []() {
// Runs on one of the pool's threads
});
// Wait for all work to complete
pool.join();
Or use it with coroutines:
asio::thread_pool pool(4);
asio::co_spawn(pool, my_coroutine(), asio::detached);
pool.join();
Best Practices
-
Prefer
any_io_executorin interfaces — Allows flexibility -
Get executor from
this_coro— Propagates the executor through the call chain -
Use strands for synchronization — Cleaner than mutexes for async code
-
Don’t assume thread identity — Unless using a strand, handlers may run on any thread
Next Steps
-
Strands — Serialize handler execution
-
Multi-Threading — Run executors from multiple threads