Migrating from ASIO Deprecated Strand to Executor-Based Synchronization
Replace asio::strand with asio::bind_executor to serialize handler execution without maintaining a separate strand object, leveraging the executor_binder wrapper for lightweight, per-handler executor association.
The chriskohlhoff/asio repository has transitioned from the legacy strand-based synchronization model to a lightweight executor-binding approach introduced in ASIO 1.38.0. This migration eliminates the need for dedicated strand objects that maintain internal shared state, replacing them with direct executor binding through asio::bind_executor as defined in include/asio/bind_executor.hpp.
Understanding the Deprecated Strand Implementation
The original synchronization mechanism relied on asio::strand<Executor>, defined in include/asio/strand.hpp, which guaranteed that handlers posted to the same strand never executed concurrently. Internally, the strand owned an implementation object of type detail::strand_executor_service::implementation_type (stored in the impl_ member) that serialized calls to the underlying executor by intercepting post, dispatch, defer, and execute operations.
The convenience function asio::make_strand() and the asio::io_context::strand class (located in include/asio/io_context_strand.hpp) both carry deprecation warnings: ASIO_DEPRECATED_MSG("Use asio::bind_executor()"). These APIs required maintaining a separate strand object that acted as an intermediary between your executor and your handler, adding unnecessary overhead and complexity to asynchronous codebases.
The Modern Executor-Based Approach
Modern ASIO code uses executor binding to associate executors directly with handlers or completion tokens. The asio::bind_executor() function returns an executor_binder<Handler, Executor> wrapper that stores the executor in its executor_ member alongside the target callable. Unlike the deprecated strand, this wrapper forwards invocations directly to the target object without requiring additional shared state objects or synchronization primitives.
The generic async-result specializations defined in include/asio/bind_executor.hpp automatically extract the executor when invoking handlers, allowing the binding to integrate seamlessly with ASIO's async initiation machinery. This design aligns with the broader execution-context model, enabling any executor to be bound on a per-handler basis rather than requiring a single strand type per execution context.
Key Architectural Differences
State Management: The deprecated asio::strand maintained mutable shared state through its internal impl_ object to track execution guarantees. The executor_binder approach is stateless regarding synchronization—it simply carries the executor as a property of the handler, relying on the underlying executor's guarantees.
Flexibility: Strands locked you into a single executor type per strand instance. With asio::bind_executor, you can bind different executors to different handlers within the same asynchronous operation chain, enabling mixed-executor pipelines.
Integration: Custom async result specializations for asio::strand were embedded directly in the strand class. The new model uses generic specializations in bind_executor.hpp that work with any handler type without additional code.
Migration Code Examples
Converting Basic Timer Handlers
The old approach required creating a strand object and binding it to the handler:
// Deprecated: Creating a strand from an io_context's executor
asio::io_context ctx;
asio::strand<asio::io_context::executor_type> s = asio::make_strand(ctx.get_executor());
void timer_handler(const asio::error_code& ec) {
// ... code that must not run concurrently ...
}
asio::steady_timer t(ctx);
t.expires_after(std::chrono::seconds(1));
t.async_wait(
asio::bind_executor(s, timer_handler) // handler posted through strand
);
The modern approach binds the executor directly without intermediate strand objects:
asio::io_context ctx;
auto exec = ctx.get_executor(); // any executor
// Bind executor directly to the callable
auto bound = asio::bind_executor(exec,
[](const asio::error_code& ec) {
// ... code that must not run concurrently ...
});
asio::steady_timer t(ctx);
t.expires_after(std::chrono::seconds(1));
t.async_wait(bound); // executor attached to token
Using Partial Tokens for Multiple Handlers
You can create a partial token to apply the same executor binding across multiple asynchronous operations:
asio::io_context ctx;
auto exec = ctx.get_executor();
auto token = asio::bind_executor(exec); // partial token
t.async_wait(token([](auto&& ec){ /* handler 1 */ }));
sock.async_read_some(buf, token([](auto&& ec, std::size_t){ /* handler 2 */ }));
Updating Coroutine-Based Code
When using asio::spawn, bind the executor directly to the coroutine handler:
asio::io_context ctx;
auto exec = ctx.get_executor();
asio::spawn(
ctx,
asio::bind_executor(exec,
[&](asio::yield_context yield) {
asio::steady_timer timer(ctx);
timer.expires_after(std::chrono::seconds(1));
timer.async_wait(yield); // runs on bound executor
}),
asio::detached);
Summary
- The
asio::strandclass ininclude/asio/strand.hppis deprecated in favor of direct executor binding viaasio::bind_executordefined ininclude/asio/bind_executor.hpp. asio::bind_executorreturns anexecutor_binderwrapper that stores the executor with the handler, eliminating the need for separate strand state management.- This migration enables per-handler executor assignment, supporting mixed-executor pipelines that were impossible with the single-executor-per-strand limitation.
- The new approach reduces overhead by removing the indirection through
strand's internalimplementation_typeobject while maintaining identical concurrency guarantees. - All new ASIO code should use
asio::bind_executor()instead ofasio::make_strand()orasio::io_context::strand.
Frequently Asked Questions
What replaced asio::strand in modern ASIO code?
asio::bind_executor replaced the standalone strand pattern. Instead of creating a asio::strand<Executor> object and maintaining it throughout your codebase, you bind the executor directly to individual handlers using the function defined in include/asio/bind_executor.hpp. This returns an executor_binder that carries the executor as part of the handler itself.
How does asio::bind_executor ensure handlers don't run concurrently?
The executor_binder wrapper does not itself provide serialization; rather, it ensures that the underlying executor—such as a strand-wrapped executor or a single-threaded context—is explicitly associated with the handler. When you bind a strand-wrapped executor (via asio::make_strand), the serialization guarantees come from the executor, not the binder. The binder simply ensures the correct executor is used for invocation.
Can I use different executors for different handlers in the same operation?
Yes. Unlike the deprecated asio::strand which required a single executor per strand instance, asio::bind_executor allows you to bind different executors to different handlers on a per-call basis. This enables complex pipelines where different stages of an asynchronous operation execute on different thread pools or contexts.
Where is the executor binding logic implemented in the ASIO source?
The core implementation resides in include/asio/bind_executor.hpp, which defines the executor_binder class template and the bind_executor() function overloads. Generic async result specializations in this file enable automatic executor extraction during handler invocation. The deprecated strand implementation remains in include/asio/strand.hpp and include/asio/io_context_strand.hpp for backward compatibility.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →