Boost.Asio Introduction: io_context, async_read,
이 글의 핵심
One thread per connection breaks at scale. Boost.Asio lets a handful of threads handle thousands of connections using async callbacks and an event loop. This guide covers the io_context model, timers, async I/O, and a minimal echo server.
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
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Learn Asio async I/O for C++: io_context, async_read, async_write, async_accept, steady_timer, error_code handling, shar… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post
- C++ Boost.Asio io_context 이벤트 루프 | 동작 원리 정리 [#1]
- C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
이 글에서 다루는 키워드 (관련 검색어)
C++, Asio, Boost.Asio, Async, io_context, Networking 등으로 검색하시면 이 글이 도움이 됩니다.