boost::asio
async operations
socket programming
strands
C++ networking

boostasio socket async_ strand

Master System Design with Codemia

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

Introduction

In Boost.Asio, a strand is a way to guarantee that certain handlers never run concurrently. That matters for socket code when several asynchronous operations share mutable connection state and the io_context may be serviced by multiple threads.

What a Strand Actually Guarantees

A strand does not make the socket itself "thread-safe" in some magical global sense. What it guarantees is that handlers dispatched through that strand are serialized. If two handlers are associated with the same strand, Boost.Asio will not execute them at the same time.

That is valuable when handlers touch shared members such as:

  • read buffers
  • write queues
  • connection state flags
  • lifetime management logic

Without a strand, those handlers may race when the io_context runs on more than one thread.

A Typical Connection Pattern

Modern Boost.Asio code often creates the socket on a strand and binds handlers to the same executor:

cpp
1#include <boost/asio.hpp>
2#include <deque>
3#include <iostream>
4#include <memory>
5#include <string>
6
7namespace asio = boost::asio;
8using tcp = asio::ip::tcp;
9
10class session : public std::enable_shared_from_this<session> {
11public:
12    explicit session(asio::io_context& io)
13        : strand_(asio::make_strand(io)),
14          socket_(strand_) {}
15
16    tcp::socket& socket() { return socket_; }
17
18    void start() { do_read(); }
19
20    void send(std::string msg) {
21        asio::dispatch(strand_, [self = shared_from_this(), msg = std::move(msg)]() mutable {
22            bool writing = !self->outbox_.empty();
23            self->outbox_.push_back(std::move(msg));
24            if (!writing) {
25                self->do_write();
26            }
27        });
28    }
29
30private:
31    void do_read() {
32        socket_.async_read_some(
33            asio::buffer(buffer_),
34            [self = shared_from_this()](boost::system::error_code ec, std::size_t n) {
35                if (!ec) {
36                    std::cout.write(self->buffer_.data(), n);
37                    std::cout << std::endl;
38                    self->do_read();
39                }
40            });
41    }
42
43    void do_write() {
44        asio::async_write(
45            socket_,
46            asio::buffer(outbox_.front()),
47            [self = shared_from_this()](boost::system::error_code ec, std::size_t) {
48                if (!ec) {
49                    self->outbox_.pop_front();
50                    if (!self->outbox_.empty()) {
51                        self->do_write();
52                    }
53                }
54            });
55    }
56
57    asio::strand<asio::io_context::executor_type> strand_;
58    tcp::socket socket_;
59    std::array<char, 1024> buffer_{};
60    std::deque<std::string> outbox_;
61};

The important part is not the exact class design. It is that all stateful read and write handlers share the same strand.

When You Need a Strand

If the io_context runs on only one thread, a strand may not be necessary because handler execution is already serialized by the single-threaded event loop. But once you run several worker threads against the same io_context, you need to think carefully about shared connection state.

A strand is often simpler than scattering mutexes through every handler. It keeps the serialization rule close to the asynchronous workflow itself.

When a Strand Is Not Enough

A strand only protects the handlers that use it. It does not serialize unrelated code that touches the same objects outside the strand. If another thread mutates connection state directly, you can still have races.

That is why the discipline matters: if a piece of state is owned by the strand, all access to that state should flow through the strand.

Common Pitfalls

  • Assuming a socket alone prevents concurrent handler execution when several threads run the io_context.
  • Binding some handlers to the strand and leaving others outside it even though they touch the same state.
  • Using a strand and then mutating the same connection members directly from unrelated threads.
  • Adding mutexes everywhere without first checking whether handler serialization by strand already solves the problem more cleanly.
  • Treating strands as performance-free, even though unnecessary serialization can reduce throughput.

Summary

  • A Boost.Asio strand guarantees that its associated handlers do not run concurrently.
  • Strands are useful when multiple async socket handlers share mutable connection state.
  • They are most relevant when an io_context is processed by multiple threads.
  • A strand only protects code that actually runs through that strand.
  • Use strands to express ownership and serialization clearly instead of mixing ad hoc locking into every handler.

Course illustration
Course illustration

All Rights Reserved.