How to Use asio io_context_strand for Serialization: A Complete Guide

The asio::io_context::strand class guarantees that handlers are never executed concurrently by serializing their execution through an internal queue, eliminating the need for manual synchronization primitives.

The asio::io_context::strand mechanism in the chriskohlhoff/asio repository provides a robust solution for serialization in asynchronous applications. When multiple threads submit work to an io_context, strands ensure that handlers execute sequentially without overlapping. This article examines the implementation details from the source code and demonstrates how to leverage strands for guaranteed serialization.

What is asio::io_context::strand?

asio::io_context::strand is a lightweight executor wrapper that ensures serialized handler execution on an underlying io_context. It leverages the internal strand_service to maintain an ordered queue and a "running" flag, preventing concurrent execution of submitted work.

According to the source code in include/asio/io_context_strand.hpp, construction binds the strand to an io_context instance (lines 99-104). The strand records handler requests and ensures the next handler does not start until the previous one completes. This architecture eliminates race conditions without requiring explicit mutexes or locks.

Creating and Using a Strand for Serialization

To create a strand for serialization, instantiate asio::io_context::strand with a valid io_context reference:

asio::io_context io;
asio::io_context::strand strand(io);  // From include/asio/io_context_strand.hpp

The strand provides three primary methods for submitting work:

Posting Handlers

The post() method schedules a handler for later execution on the io_context:

strand.post([]{ 
    std::cout << "Handler executed" << std::endl; 
}, allocator);

This guarantees the handler runs only after any previously submitted strand handlers complete.

Dispatching vs. Deferring

The dispatch() method may execute the handler immediately if the strand is idle, while defer() always schedules the handler for later execution:

  • strand.dispatch(handler) – Runs immediately if called from within the strand's execution context; otherwise behaves like post().
  • strand.defer(handler) – Always queues the handler, ensuring no immediate execution.

Both methods ultimately invoke the static member functions of asio::detail::strand_service (defined in include/asio/detail/strand_service.hpp), which maintain the internal queue and serialization state.

Basic Serialization Example

The following example demonstrates guaranteed serialization using post():

#include <asio.hpp>
#include <iostream>
#include <thread>
#include <chrono>

void print_message(const std::string& msg)
{
    std::cout << "Start: " << msg << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "End:   " << msg << std::endl;
}

int main()
{
    asio::io_context io;
    asio::io_context::strand strand(io);

    // These handlers execute sequentially, never concurrently
    strand.post([]{ print_message("A"); });
    strand.post([]{ print_message("B"); });
    strand.post([]{ print_message("C"); });

    io.run();
}

Output shows deterministic ordering without interleaving:


Start: A
End:   A
Start: B
End:   B
Start: C
End:   C

Thread Safety and Concurrency Guarantees

The strand mechanism protects against concurrent submissions from multiple threads. Because strands are lightweight and copyable, copies share the same underlying state (see the copy constructor in include/asio/io_context_strand.hpp, lines 107-114).

Multi-Threaded Serialization

Even when threads race to submit work, the strand maintains serialization:

std::thread t1([&]{ strand.post([]{ print_message("T1-1"); }); });
std::thread t2([&]{ strand.post([]{ print_message("T2-1"); }); });
t1.join(); 
t2.join();
io.run();  // Executes T1-1 then T2-1 (or vice versa), never interleaved

Checking Execution Context

The running_in_this_thread() method returns true if the current thread is executing a handler submitted through the strand:

if (strand.running_in_this_thread()) {
    // Safe to access strand-protected resources without posting
}

Modern Executor-Based Alternative

For code using the newer executor-based API, the generic asio::strand class provides identical serialization guarantees. Defined in include/asio/strand.hpp, it works with any executor type:

asio::io_context io;
auto exec = asio::make_strand(io.get_executor());
exec.post([]{ print_message("executor-strand"); });
io.run();

Both asio::io_context::strand and asio::strand share the same underlying implementation via strand_executor_service (see include/asio/detail/strand_executor_service.hpp), ensuring consistent behavior across APIs.

Key Implementation Files

The serialization mechanism relies on the following source files in the chriskohlhoff/asio repository:

Summary

  • asio::io_context::strand guarantees serialized handler execution without manual synchronization.
  • Use post() for deferred execution, dispatch() for potential immediate execution, and defer() for guaranteed queuing.
  • Strands are copyable and lightweight, with copies sharing the same serialization state.
  • The running_in_this_thread() method allows context-aware logic inside strand handlers.
  • Modern code should consider asio::strand with make_strand() for executor compatibility.

Frequently Asked Questions

What is the difference between strand.post() and strand.dispatch()?

strand.post() always schedules the handler for later execution on the io_context, while strand.dispatch() may execute the handler immediately if called from within the strand's execution context. If the current thread is not running a strand handler, dispatch() behaves identically to post().

Can I copy an io_context::strand between threads?

Yes. The strand is lightweight and copyable (see lines 107-114 in include/asio/io_context_strand.hpp), and copies share the same underlying state. This allows passing strands across thread boundaries while maintaining the serialization guarantee for all submitted work.

How does strand serialization compare to using a mutex?

Strands provide implicit synchronization through the io_context event loop rather than blocking threads. Unlike mutexes, strands never cause contention or context switches because handlers execute sequentially on the same thread. This model is often more efficient for asynchronous I/O operations than traditional locking.

Should I use io_context::strand or asio::strand in new code?

For new code using Asio 1.12 or later, prefer asio::strand with asio::make_strand() as defined in include/asio/strand.hpp. This generic executor-based approach integrates better with modern C++ executors and networking TS proposals, while providing identical serialization semantics to the legacy io_context::strand.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →