Boost.Asio Introduction: io_context, async_read,
Why Async I/O?
The traditional approach to handling network connections is one thread per connection. It works fine for tens of connections — and breaks down at thousands.
The problem with one-thread-per-connection:
1,000 connections × 1 MB stack per thread = 1 GB just for stacks
Most threads blocked in read() waiting for data
Context switching overhead grows with connection count
Async I/O with Asio:
Small thread pool (e.g., 4 threads for 16 cores)
Threads execute handlers when I/O completes
Waiting for data costs zero CPU and minimal memory
10,000 idle connections = 10,000 registered file descriptors, not 10,000 threads
Asio wraps the OS-level event notification (epoll on Linux, kqueue on macOS, IOCP on Windows) behind a consistent async interface.
Core Concepts
io_context
io_context is the event loop. It tracks pending async operations and dispatches completion handlers:
#include <boost/asio.hpp>
namespace asio = boost::asio;
asio::io_context ioc;
// Queue a simple task
asio::post(ioc, [] {
std::cout << "Hello from io_context\n";
});
// Run until no pending work
ioc.run();
// Output: Hello from io_context
Key methods:
run()— blocks until all pending handlers are dispatchedpoll()— runs ready handlers without blocking (useful for game loops)stop()— signals run() to returnrestart()— resets after run() exits, so you can run() again
work_guard — Keeping the Loop Alive
Without pending work, run() returns immediately. Use work_guard to keep it alive:
auto guard = asio::make_work_guard(ioc);
std::thread t([&ioc] { ioc.run(); });
// ... do async work in other threads ...
guard.reset(); // allow run() to return when work finishes
t.join();
Async Timer
steady_timer is the simplest async operation — useful for timeouts, delays, and periodic tasks:
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <iostream>
namespace asio = boost::asio;
int main() {
asio::io_context ioc;
asio::steady_timer timer(ioc, std::chrono::seconds(2));
timer.async_wait([](const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Timer fired after 2 seconds\n";
}
});
ioc.run();
}
Periodic Timer (Re-armed in Handler)
class PeriodicTask {
asio::steady_timer timer_;
std::chrono::milliseconds interval_;
public:
PeriodicTask(asio::io_context& ioc, std::chrono::milliseconds interval)
: timer_(ioc), interval_(interval) {
schedule();
}
private:
void schedule() {
timer_.expires_after(interval_);
timer_.async_wait([this](const boost::system::error_code& ec) {
if (!ec) {
doWork();
schedule(); // re-arm for next tick
}
});
}
void doWork() {
std::cout << "Tick at "
<< std::chrono::steady_clock::now().time_since_epoch().count()
<< '\n';
}
};
Async TCP Client
A typical async client chains operations: resolve → connect → write → read.
#include <boost/asio.hpp>
#include <iostream>
#include <memory>
#include <string>
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
class TcpClient : public std::enable_shared_from_this<TcpClient> {
tcp::socket socket_;
asio::streambuf buf_;
std::string request_;
public:
explicit TcpClient(asio::io_context& ioc)
: socket_(ioc) {}
void connect(const std::string& host, const std::string& port, const std::string& msg) {
request_ = msg;
tcp::resolver resolver(socket_.get_executor());
auto endpoints = resolver.resolve(host, port);
asio::async_connect(socket_, endpoints,
[self = shared_from_this()](boost::system::error_code ec, tcp::endpoint) {
if (!ec) self->write();
});
}
private:
void write() {
asio::async_write(socket_, asio::buffer(request_),
[self = shared_from_this()](boost::system::error_code ec, std::size_t) {
if (!ec) self->read();
});
}
void read() {
asio::async_read_until(socket_, buf_, '\n',
[self = shared_from_this()](boost::system::error_code ec, std::size_t) {
if (!ec) {
std::istream is(&self->buf_);
std::string line;
std::getline(is, line);
std::cout << "Response: " << line << '\n';
}
});
}
};
int main() {
asio::io_context ioc;
auto client = std::make_shared<TcpClient>(ioc);
client->connect("localhost", "8080", "hello\n");
ioc.run();
}
Async TCP Echo Server
The server pattern: accept → spawn session → loop (read → write → read):
#include <boost/asio.hpp>
#include <iostream>
#include <memory>
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
// Session handles one connection lifetime
class Session : public std::enable_shared_from_this<Session> {
tcp::socket socket_;
asio::streambuf buf_;
public:
explicit Session(tcp::socket socket)
: socket_(std::move(socket)) {}
void start() { read(); }
private:
void read() {
asio::async_read_until(socket_, buf_, '\n',
[self = shared_from_this()](boost::system::error_code ec, std::size_t bytes) {
if (!ec) {
self->write(bytes);
} else if (ec != asio::error::eof) {
std::cerr << "Read error: " << ec.message() << '\n';
}
// eof = clean close, just let the session die
});
}
void write(std::size_t bytes) {
// Echo back exactly what we read (including the newline)
asio::async_write(socket_, buf_,
[self = shared_from_this()](boost::system::error_code ec, std::size_t) {
if (!ec) {
self->read(); // wait for next message
}
});
}
};
class Server {
tcp::acceptor acceptor_;
public:
Server(asio::io_context& ioc, unsigned short port)
: acceptor_(ioc, {tcp::v4(), port}) {
accept();
}
private:
void accept() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket))->start();
}
accept(); // always re-arm for next connection
});
}
};
int main() {
asio::io_context ioc;
Server server(ioc, 8080);
std::cout << "Echo server on :8080\n";
ioc.run();
}
Error Handling
Always check error_code in every handler. Don’t throw in handlers — it propagates to io_context::run() and is usually unhandled:
void handleRead(boost::system::error_code ec, std::size_t bytes) {
if (ec == asio::error::eof) {
// Client closed connection cleanly — not an error
shutdown();
return;
}
if (ec == asio::error::operation_aborted) {
// Operation was cancelled (e.g., timer expired, socket closed)
return;
}
if (ec) {
// connection_reset, broken_pipe, etc.
std::cerr << "I/O error: " << ec.message() << '\n';
shutdown();
return;
}
// Process bytes...
}
Common Mistakes
1. Dangling this in handlers
Handlers can execute after the object is destroyed. Always use shared_from_this():
// Wrong
socket_.async_read_some(buf, [this](auto ec, auto n) { /* this might be dead */ });
// Right — extends lifetime until handler runs
socket_.async_read_some(buf, [self = shared_from_this()](auto ec, auto n) { /* safe */ });
2. Stack-allocated buffers
Async operations keep a pointer to the buffer. Stack memory is gone when the enclosing function returns:
// Wrong — buffer destroyed before async_write completes
std::string msg = "hello\n";
asio::async_write(socket_, asio::buffer(msg), handler);
// Right — buffer lives in the session object or heap
// (member variable, shared_ptr<std::string>, etc.)
3. Forgetting to re-accept
If you don’t call async_accept again in the accept handler, the server stops accepting after the first connection:
acceptor_.async_accept([this](auto ec, auto socket) {
if (!ec) handleNewConnection(std::move(socket));
accept(); // ALWAYS re-arm — even on error (unless you want to stop)
});
4. Running io_context twice without restart
After run() returns, you must call restart() before running again:
ioc.run();
// ... some setup ...
ioc.restart(); // required
ioc.run(); // now works
Thread Pool Pattern
For production servers, run the io_context on multiple threads:
asio::io_context ioc;
auto guard = asio::make_work_guard(ioc);
// Thread pool — 4 threads share the event loop
std::vector<std::thread> pool;
for (int i = 0; i < 4; ++i) {
pool.emplace_back([&ioc] { ioc.run(); });
}
// When done:
guard.reset();
for (auto& t : pool) t.join();
When multiple threads run the same io_context, handlers may run concurrently. Use asio::strand to serialize handlers that share state:
auto strand = asio::make_strand(ioc);
// These two handlers will never run simultaneously
asio::post(strand, [] { /* access shared socket */ });
asio::post(strand, [] { /* access shared socket */ });
Key Takeaways
io_contextis the event loop —run()dispatches completion handlers until there’s no more work- Async operations take a callback; they return immediately and call the handler when I/O completes
- Always use
shared_from_this()in handlers to prevent use-after-free when an object outlives its async operations - Never use stack-allocated buffers for async operations — use member variables or
shared_ptr - Always re-arm
async_acceptin the accept handler or the server stops accepting after one connection asio::strandserializes handlers when multiple threads share anio_context, avoiding data races without explicit mutexes
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Learn Asio async I/O for C++: io_context, async_read, async_write, async_accept, steady_timer, error_code handling.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post
- C++ Boost.Asio io_context 이벤트 루프 | 동작 원리 정리 [#1]
- C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
Keywords Covered in This Article (Related Search Terms)
This article covers C++, Asio, Boost.Asio, Async, io_context, Networking.