본문으로 건너뛰기
Previous
Next
Boost.Asio Introduction: io_context, async_read,

Boost.Asio Introduction: io_context, async_read,

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 dispatched
  • poll() — runs ready handlers without blocking (useful for game loops)
  • stop() — signals run() to return
  • restart() — 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_context is 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_accept in the accept handler or the server stops accepting after one connection
  • asio::strand serializes handlers when multiple threads share an io_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++, Asio, Boost.Asio, Async, io_context, Networking 등으로 검색하시면 이 글이 도움이 됩니다.