C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post

C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post

이 글의 핵심

Asio 이벤트 루프의 모든 것: run/run_one/poll 차이, post/dispatch 작업 큐, work_guard로 서버 유지, strand 동기화, C++20 코루틴, 일반적인 에러와 프로덕션 패턴까지 실전 코드로 완벽 정리.

들어가며: “서버가 바로 종료돼요. run()이 끝나지 않게 하려면?”

문제 상황 1: 서버가 즉시 종료됨

// ❌ 문제: 서버가 바로 종료됨
boost::asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));

// async_accept 한 번만 등록
acceptor.async_accept( {
    std::cout << "Connected!\n";
    // 여기서 끝
});

io.run();  // 💥 연결 하나 받고 바로 종료!
std::cout << "Server stopped\n";  // 즉시 출력됨

왜 이런 일이 발생할까요?

io_context::run()등록된 비동기 작업이 모두 완료되면 반환합니다. 위 코드는 async_accept 하나만 등록했으므로, 연결 하나를 받으면 더 이상 할 일이 없어서 run()이 종료됩니다.

추가 문제 시나리오

시나리오 2: run()이 끝나지 않아요
work_guard를 사용했는데 reset()을 호출하지 않아 서버를 종료할 수 없는 경우. SIGINT 핸들러에서 work_guard.reset()을 호출해야 graceful shutdown이 가능합니다.

시나리오 3: 멀티스레드에서 데이터 레이스
여러 스레드가 같은 io_context::run()을 호출할 때, 공유 변수에 mutex 없이 접근하면 undefined behavior가 발생합니다. strand로 순차 실행을 보장해야 합니다.

시나리오 4: 핸들러 실행 순서 혼란
postdispatch의 차이를 모르고 사용하면, 핸들러가 예상과 다른 순서로 실행될 수 있습니다. dispatch는 현재 핸들러 내부에서 즉시 실행되므로 재귀 깊이에 주의해야 합니다.

시나리오 5: run() 호출 후 io_context 재사용
io.run()이 반환된 io_context는 “stopped” 상태입니다. io.restart()를 호출하지 않고 다시 run()을 호출하면 아무 작업도 실행되지 않습니다.

해결책:

  1. work_guard: “아직 할 일이 있다”고 표시
  2. 완료 핸들러에서 재등록: async_accept 완료 시 다시 async_accept 등록
  3. 멀티스레드 run(): 여러 스레드가 동시에 이벤트 처리

목표:

  • run() / run_one() / poll() 동작 이해
  • post / dispatch로 작업 큐 관리
  • work_guard로 서버 유지
  • 멀티스레드 이벤트 루프 구현
  • 완료 핸들러 체이닝 패턴

요구 환경: Boost.Asio 1.70 이상

이 글을 읽으면:

  • 이벤트 루프의 동작 원리를 이해할 수 있습니다.
  • 서버가 종료되지 않게 유지할 수 있습니다.
  • 멀티스레드로 성능을 향상시킬 수 있습니다.

개념을 잡는 비유

소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


목차

  1. 이벤트 루프 동작 원리
  2. run/run_one/poll 비교
  3. work_guard로 서버 유지
  4. post와 dispatch
  5. 멀티스레드 이벤트 루프
  6. strand 완전 예제
  7. C++20 코루틴
  8. 완료 핸들러 체이닝
  9. 실전 예시
  10. 일반적인 에러와 해결법
  11. 모범 사례
  12. 프로덕션 패턴

1. 이벤트 루프 동작 원리

이벤트 루프란?

flowchart TB
    Start["io_context run 시작"]
    Check{등록된<br/>작업 있음?}
    Wait[I/O 이벤트 대기]
    Execute[완료 핸들러 실행]
    Done[run 종료]
    
    Start --> Check
    Check -->|Yes| Wait
    Wait --> Execute
    Execute --> Check
    Check -->|No| Done
    
    style Wait fill:#ffeb3b
    style Execute fill:#4caf50
    style Done fill:#f44336

이벤트 루프의 핵심:

  1. 등록된 비동기 작업을 확인
  2. I/O 이벤트 발생 대기 (epoll/kqueue/IOCP)
  3. 이벤트 발생 시 완료 핸들러 실행
  4. 1번으로 돌아가서 반복
  5. 더 이상 작업이 없으면 종료

시퀀스 다이어그램: run() 동작

sequenceDiagram
    participant Main as 메인 스레드
    participant IO as io_context
    participant Kernel as OS (epoll/kqueue)
    
    Main->>IO: post(핸들러1), post(핸들러2)
    Main->>IO: run() 호출
    IO->>IO: 작업 큐 확인 (2개)
    loop 이벤트 루프
        IO->>Kernel: 이벤트 대기 (또는 즉시 실행)
        Kernel-->>IO: 준비된 작업
        IO->>IO: 핸들러1 실행
        IO->>IO: 핸들러2 실행
        IO->>IO: 작업 없음?
    end
    IO-->>Main: run() 반환

내부 동작 이해

#include <boost/asio.hpp>
#include <iostream>
#include <thread>

using boost::asio::ip::tcp;
using boost::system::error_code;

void demonstrate_event_loop() {
    boost::asio::io_context io;
    
    std::cout << "1. run() 호출 전\n";
    
    // 비동기 작업 등록
    boost::asio::post(io,  {
        std::cout << "2. 첫 번째 핸들러 실행\n";
    });
    
    boost::asio::post(io,  {
        std::cout << "3. 두 번째 핸들러 실행\n";
    });
    
    std::cout << "4. run() 호출\n";
    io.run();  // 여기서 2, 3번 핸들러 실행
    
    std::cout << "5. run() 종료 (더 이상 작업 없음)\n";
}

// 출력:
// 1. run() 호출 전
// 4. run() 호출
// 2. 첫 번째 핸들러 실행
// 3. 두 번째 핸들러 실행
// 5. run() 종료 (더 이상 작업 없음)

2. run/run_one/poll 비교

세 가지 실행 방식

#include <boost/asio.hpp>
#include <iostream>

void compare_run_methods() {
    boost::asio::io_context io;
    
    // 작업 3개 등록
    for (int i = 1; i <= 3; ++i) {
        boost::asio::post(io, [i]() {
            std::cout << "Task " << i << "\n";
        });
    }
    
    // 1. run(): 모든 작업 실행
    {
        boost::asio::io_context io1;
        for (int i = 1; i <= 3; ++i) {
            boost::asio::post(io1, [i]() {
                std::cout << "run: Task " << i << "\n";
            });
        }
        io1.run();  // Task 1, 2, 3 모두 실행
        std::cout << "run() completed\n";
    }
    
    // 2. run_one(): 한 번에 하나씩
    {
        boost::asio::io_context io2;
        for (int i = 1; i <= 3; ++i) {
            boost::asio::post(io2, [i]() {
                std::cout << "run_one: Task " << i << "\n";
            });
        }
        
        io2.run_one();  // Task 1만 실행
        std::cout << "First run_one() completed\n";
        
        io2.run_one();  // Task 2만 실행
        std::cout << "Second run_one() completed\n";
        
        io2.run();  // Task 3 실행
    }
    
    // 3. poll(): 대기 없이 준비된 작업만
    {
        boost::asio::io_context io3;
        
        // 즉시 실행 가능한 작업
        boost::asio::post(io3,  {
            std::cout << "poll: Immediate task\n";
        });
        
        // I/O 대기가 필요한 작업 (타이머)
        boost::asio::steady_timer timer(io3, std::chrono::seconds(1));
        timer.async_wait( {
            std::cout << "poll: Timer expired\n";
        });
        
        io3.poll();  // Immediate task만 실행 (타이머는 실행 안 됨)
        std::cout << "poll() completed (no blocking)\n";
        
        // 타이머 만료까지 대기하려면 run() 필요
        io3.run();  // Timer expired 실행
    }
}

비교표

메서드동작사용 사례
run()모든 작업 완료까지 블로킹서버 메인 루프
run_one()한 작업만 실행 후 반환작업 단위 제어
poll()대기 없이 준비된 작업만게임 루프, UI 업데이트
run_for(duration)시간 제한 실행타임아웃 필요 시
run_until(time_point)특정 시각까지 실행스케줄링

3. work_guard로 서버 유지

문제: 서버가 바로 종료됨

// ❌ 잘못된 서버 코드
void broken_server() {
    boost::asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    
    acceptor.async_accept( {
        std::cout << "Client connected\n";
        // 한 번만 실행되고 끝
    });
    
    io.run();  // 연결 하나 받고 종료!
    std::cout << "Server stopped\n";
}

해결책 1: work_guard 사용

#include <boost/asio.hpp>
#include <iostream>

void server_with_work_guard() {
    boost::asio::io_context io;
    
    // work_guard 생성: "아직 할 일이 있다"고 표시
    auto work = boost::asio::make_work_guard(io);
    
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    
    acceptor.async_accept( {
        std::cout << "Client connected\n";
    });
    
    std::cout << "Server started on port 8080\n";
    
    // work_guard가 있으므로 run()이 종료되지 않음
    std::thread server_thread([&io]() {
        io.run();
    });
    
    // 5초 후 종료
    std::this_thread::sleep_for(std::chrono::seconds(5));
    
    // work_guard 해제 → run() 종료
    work.reset();
    
    server_thread.join();
    std::cout << "Server stopped\n";
}

해결책 2: 완료 핸들러에서 재등록 (권장)

class Server {
    boost::asio::io_context& io_;
    tcp::acceptor acceptor_;
    
public:
    Server(boost::asio::io_context& io, uint16_t port)
        : io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
        start_accept();
    }
    
private:
    void start_accept() {
        // ✅ 핵심: 완료 핸들러에서 다시 start_accept() 호출
        acceptor_.async_accept(
            [this](error_code ec, tcp::socket socket) {
                if (!ec) {
                    std::cout << "Client connected from "
                              << socket.remote_endpoint() << "\n";
                    
                    // 클라이언트 처리 (세션 시작)
                    handle_client(std::move(socket));
                }
                
                // 다음 연결 대기 (재귀적 등록)
                start_accept();
            }
        );
    }
    
    void handle_client(tcp::socket socket) {
        // 클라이언트와 통신
        auto buffer = std::make_shared<std::array<char, 1024>>();
        
        socket.async_read_some(
            boost::asio::buffer(*buffer),
            [buffer, socket = std::move(socket)](error_code ec, size_t bytes) mutable {
                if (!ec) {
                    std::cout << "Received " << bytes << " bytes\n";
                    
                    // Echo back
                    boost::asio::async_write(
                        socket,
                        boost::asio::buffer(*buffer, bytes),
                         {}
                    );
                }
            }
        );
    }
};

void run_server() {
    boost::asio::io_context io;
    Server server(io, 8080);
    
    std::cout << "Server started on port 8080\n";
    io.run();  // 계속 실행됨 (async_accept가 계속 등록되므로)
}

4. post와 dispatch

post: 항상 큐에 넣기

void demonstrate_post() {
    boost::asio::io_context io;
    
    std::cout << "Main thread: " << std::this_thread::get_id() << "\n";
    
    // post: 항상 나중에 실행
    boost::asio::post(io,  {
        std::cout << "Handler thread: " << std::this_thread::get_id() << "\n";
        std::cout << "This runs later\n";
    });
    
    std::cout << "Before run()\n";
    io.run();
    std::cout << "After run()\n";
}

// 출력:
// Main thread: 123456
// Before run()
// Handler thread: 123456
// This runs later
// After run()

dispatch: 가능하면 즉시 실행

void demonstrate_dispatch() {
    boost::asio::io_context io;
    
    // dispatch: run() 실행 중이면 즉시 실행 가능
    boost::asio::dispatch(io,  {
        std::cout << "Dispatch 1\n";
        
        // 핸들러 내부에서 dispatch → 즉시 실행
        boost::asio::dispatch(io,  {
            std::cout << "Dispatch 2 (immediate)\n";
        });
        
        // 핸들러 내부에서 post → 큐에 넣음
        boost::asio::post(io,  {
            std::cout << "Post (queued)\n";
        });
        
        std::cout << "Dispatch 1 end\n";
    });
    
    io.run();
}

// 출력:
// Dispatch 1
// Dispatch 2 (immediate)
// Dispatch 1 end
// Post (queued)

언제 무엇을 사용할까?

상황사용이유
다른 스레드에서 작업 등록post스레드 안전
핸들러 내부에서 작업 등록dispatch오버헤드 감소
순서 보장 필요post큐 순서 보장
즉시 실행 가능dispatch성능 최적화

5. 멀티스레드 이벤트 루프

단일 스레드 vs 멀티스레드

// 단일 스레드: 한 번에 하나씩 처리
void single_threaded_server() {
    boost::asio::io_context io;
    // ... acceptor 설정 ...
    
    io.run();  // 메인 스레드에서만 실행
}

// 멀티스레드: 여러 핸들러 동시 처리
void multi_threaded_server() {
    boost::asio::io_context io;
    // ... acceptor 설정 ...
    
    // 4개 스레드가 같은 io_context 처리
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back([&io]() {
            io.run();
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
}

스레드 풀 구현

class ThreadPool {
    boost::asio::io_context io_;
    boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_;
    std::vector<std::thread> threads_;
    
public:
    ThreadPool(size_t num_threads)
        : work_(boost::asio::make_work_guard(io_)) {
        
        for (size_t i = 0; i < num_threads; ++i) {
            threads_.emplace_back([this]() {
                io_.run();
            });
        }
    }
    
    ~ThreadPool() {
        work_.reset();  // work_guard 해제
        
        for (auto& t : threads_) {
            t.join();
        }
    }
    
    // 작업 추가
    template<typename F>
    void post(F&& f) {
        boost::asio::post(io_, std::forward<F>(f));
    }
    
    boost::asio::io_context& get_io_context() {
        return io_;
    }
};

// 사용 예시
void use_thread_pool() {
    ThreadPool pool(4);  // 4개 스레드
    
    // 작업 추가
    for (int i = 0; i < 10; ++i) {
        pool.post([i]() {
            std::cout << "Task " << i 
                      << " on thread " << std::this_thread::get_id() << "\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        });
    }
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

동기화 주의사항

class Counter {
    int count_ = 0;
    std::mutex mutex_;  // ❌ 멀티스레드에서 필요
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++count_;
    }
    
    int get() {
        std::lock_guard<std::mutex> lock(mutex_);
        return count_;
    }
};

// ✅ strand 사용 (Asio의 동기화 메커니즘)
class StrandCounter {
    boost::asio::io_context::strand strand_;
    int count_ = 0;  // strand로 보호되므로 mutex 불필요
    
public:
    StrandCounter(boost::asio::io_context& io)
        : strand_(io) {}
    
    void increment() {
        boost::asio::post(strand_, [this]() {
            ++count_;  // strand 내에서 실행 → 순차 보장
        });
    }
    
    void get(std::function<void(int)> callback) {
        boost::asio::post(strand_, [this, callback]() {
            callback(count_);
        });
    }
};

6. strand 완전 예제

strand는 같은 io_context에서 순차 실행을 보장하는 실행 컨텍스트입니다. mutex 없이 공유 자원을 안전하게 접근할 수 있습니다.

strand로 보호된 Echo 세션

#include <boost/asio.hpp>
#include <memory>

using boost::asio::ip::tcp;
using boost::system::error_code;

class StrandEchoSession : public std::enable_shared_from_this<StrandEchoSession> {
    tcp::socket socket_;
    boost::asio::io_context::strand strand_;
    std::array<char, 1024> buffer_;
    
public:
    StrandEchoSession(tcp::socket socket)
        : socket_(std::move(socket)),
          strand_(socket_.get_executor()) {}
    
    void start() {
        boost::asio::dispatch(strand_, [self = shared_from_this()]() {
            self->do_read();
        });
    }
    
private:
    void do_read() {
        auto self = shared_from_this();
        socket_.async_read_some(
            boost::asio::buffer(buffer_),
            boost::asio::bind_executor(strand_, [this, self](error_code ec, size_t bytes) {
                if (!ec) do_write(bytes);
            })
        );
    }
    
    void do_write(size_t bytes) {
        auto self = shared_from_this();
        boost::asio::async_write(
            socket_, boost::asio::buffer(buffer_, bytes),
            boost::asio::bind_executor(strand_, [this, self](error_code ec, size_t) {
                if (!ec) do_read();
            })
        );
    }
};

strand vs mutex

방식장점단점
strand데드락 없음, Asio 네이티브strand 범위 설계 필요
mutex기존 코드와 호환데드락 위험, 성능 오버헤드

7. C++20 코루틴

Boost.Asio는 C++20 코루틴을 지원합니다. co_await로 콜백 지옥을 피하고 동기 코드처럼 작성할 수 있습니다.

Echo 서버 (코루틴)

#if __cplusplus >= 202002L

namespace asio = boost::asio;
using asio::ip::tcp;

asio::awaitable<void> echo_session(tcp::socket socket) {
    try {
        char data[1024];
        for (;;) {
            std::size_t n = co_await socket.async_read_some(
                asio::buffer(data), asio::use_awaitable);
            co_await asio::async_write(
                socket, asio::buffer(data, n), asio::use_awaitable);
        }
    } catch (const std::exception& e) {
        std::printf("Echo exception: %s\n", e.what());
    }
}

asio::awaitable<void> listen(tcp::acceptor& acceptor) {
    for (;;) {
        tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
        asio::co_spawn(
            acceptor.get_executor(),
            echo_session(std::move(socket)),
            asio::detached);
    }
}

void run_coroutine_server() {
    asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    asio::co_spawn(io, listen(acceptor), asio::detached);
    io.run();
}

#endif  // C++20

에러 처리: as_tuple

auto [ec, n] = co_await socket.async_read_some(
    asio::buffer(data), asio::as_tuple(asio::use_awaitable));
if (ec) {
    std::cerr << "Read error: " << ec.message() << "\n";
    co_return;
}

8. 완료 핸들러 체이닝

패턴: 완료 시 다음 작업 등록

class EchoSession : public std::enable_shared_from_this<EchoSession> {
    tcp::socket socket_;
    std::array<char, 1024> buffer_;
    
public:
    EchoSession(tcp::socket socket)
        : socket_(std::move(socket)) {}
    
    void start() {
        do_read();
    }
    
private:
    void do_read() {
        auto self = shared_from_this();
        
        socket_.async_read_some(
            boost::asio::buffer(buffer_),
            [this, self](error_code ec, size_t bytes) {
                if (!ec) {
                    do_write(bytes);  // ✅ 읽기 완료 → 쓰기 시작
                }
            }
        );
    }
    
    void do_write(size_t bytes) {
        auto self = shared_from_this();
        
        boost::asio::async_write(
            socket_,
            boost::asio::buffer(buffer_, bytes),
            [this, self](error_code ec, size_t) {
                if (!ec) {
                    do_read();  // ✅ 쓰기 완료 → 다시 읽기
                }
            }
        );
    }
};

타이머 체이닝

class PeriodicTimer {
    boost::asio::steady_timer timer_;
    std::function<void()> callback_;
    std::chrono::milliseconds interval_;
    
public:
    PeriodicTimer(
        boost::asio::io_context& io,
        std::chrono::milliseconds interval,
        std::function<void()> callback
    ) : timer_(io), interval_(interval), callback_(callback) {}
    
    void start() {
        schedule_next();
    }
    
private:
    void schedule_next() {
        timer_.expires_after(interval_);
        
        timer_.async_wait([this](error_code ec) {
            if (!ec) {
                callback_();
                schedule_next();  // ✅ 타이머 완료 → 다시 등록
            }
        });
    }
};

// 사용
void use_periodic_timer() {
    boost::asio::io_context io;
    
    PeriodicTimer timer(io, std::chrono::seconds(1),  {
        std::cout << "Tick: " << std::time(nullptr) << "\n";
    });
    
    timer.start();
    io.run();
}

9. 실전 예시

예시 1: HTTP 서버 (간단한 버전)

class SimpleHttpServer {
    boost::asio::io_context& io_;
    tcp::acceptor acceptor_;
    
public:
    SimpleHttpServer(boost::asio::io_context& io, uint16_t port)
        : io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
        start_accept();
    }
    
private:
    void start_accept() {
        acceptor_.async_accept([this](error_code ec, tcp::socket socket) {
            if (!ec) {
                handle_request(std::move(socket));
            }
            start_accept();  // 다음 연결 대기
        });
    }
    
    void handle_request(tcp::socket socket) {
        auto buffer = std::make_shared<boost::asio::streambuf>();
        
        boost::asio::async_read_until(
            socket,
            *buffer,
            "\r\n\r\n",
            [this, socket = std::move(socket), buffer](error_code ec, size_t) mutable {
                if (!ec) {
                    std::string response =
                        "HTTP/1.1 200 OK\r\n"
                        "Content-Length: 13\r\n"
                        "\r\n"
                        "Hello, World!";
                    
                    boost::asio::async_write(
                        socket,
                        boost::asio::buffer(response),
                         {}
                    );
                }
            }
        );
    }
};

타임아웃 처리 패턴

// 타이머로 읽기 타임아웃 구현 (콜백 방식)
void read_with_timeout(std::shared_ptr<tcp::socket> socket,
    boost::asio::mutable_buffer buffer,
    std::chrono::seconds timeout,
    std::function<void(error_code, size_t)> handler) {
    
    auto timer = std::make_shared<boost::asio::steady_timer>(
        socket->get_executor(), timeout);
    auto buf = std::make_shared<std::vector<char>>(buffer.size());
    
    timer->async_wait([socket, handler](error_code ec) {
        if (!ec) socket->cancel();  // 타임아웃 시 읽기 취소
    });
    
    socket->async_read_some(boost::asio::buffer(*buf),
        [timer, buf, handler](error_code ec, size_t n) mutable {
            timer->cancel();  // 읽기 완료 시 타이머 취소
            handler(ec, n);
        });
}

연결 풀 (클라이언트 측)

class ConnectionPool {
    boost::asio::io_context& io_;
    std::queue<std::shared_ptr<tcp::socket>> pool_;
    std::string host_, port_;
    size_t max_size_;
    
public:
    void acquire(std::function<void(error_code, std::shared_ptr<tcp::socket>)> cb) {
        if (!pool_.empty()) {
            auto sock = std::move(pool_.front());
            pool_.pop();
            cb(error_code{}, std::move(sock));
            return;
        }
        // resolver로 새 연결 생성 후 cb 호출
    }
    
    void release(std::shared_ptr<tcp::socket> sock) {
        if (pool_.size() < max_size_ && sock->is_open())
            pool_.push(std::move(sock));
    }
};

10. 일반적인 에러와 해결법

에러 1: shared_from_this() 호출 시 bad_weak_ptr

원인: 객체가 아직 shared_ptr로 관리되지 않은 상태에서 shared_from_this() 호출.

// ❌ 잘못된 코드
new Session(socket)->start();  // shared_ptr 아님 → bad_weak_ptr

해결법:

// ✅ 올바른 코드
auto session = std::make_shared<Session>(std::move(socket));
session->start();

에러 2: run() 후 io_context 재사용

원인: run()이 반환된 io_context는 stopped 상태.

// ❌ 잘못된 코드
io.run();  // 완료 후
boost::asio::post(io, {});
io.run();  // 💥 아무것도 실행 안 됨

해결법:

// ✅ 올바른 코드
io.restart();
io.run();

에러 3: 소켓/버퍼 수명 관리

원인: 비동기 작업 완료 전에 소켓이나 버퍼가 소멸됨.

// ❌ 잘못된 코드
std::array<char, 1024> buffer;  // 스택
socket.async_read_some(boost::asio::buffer(buffer), {});
// 함수 종료 → buffer 소멸

해결법:

// ✅ 올바른 코드
auto buffer = std::make_shared<std::array<char, 1024>>();
socket.async_read_some(
    boost::asio::buffer(*buffer),
    [buffer, socket = std::move(socket)](error_code ec, size_t n) mutable {});

에러 4: 멀티스레드에서 공유 변수 접근

원인: 여러 스레드가 io.run() 실행 시, 핸들러가 서로 다른 스레드에서 실행됨.

해결법: strand 사용 또는 std::mutex 사용.

// ✅ strand 사용
boost::asio::io_context::strand strand(io);
boost::asio::post(strand, [&]() { ++counter; });

에러 5: dispatch 재귀 깊이

원인: 핸들러 내부에서 dispatch로 자기 자신 호출 → 스택 오버플로우.

해결법: 재귀가 깊어지면 post 사용 (큐에 넣어 스택 해제 후 실행).


11. 모범 사례

  1. shared_from_this: 세션 클래스는 enable_shared_from_this 상속, make_shared로 생성
  2. strand: 멀티스레드 run() 사용 시 공유 상태는 전용 strand로 보호
  3. 에러 코드: 모든 비동기 핸들러에서 error_code 확인
  4. post vs dispatch: 다른 스레드 → post, 핸들러 내부 → dispatch (재귀 주의)
  5. work_guard: graceful shutdown에서 reset() 호출 시점을 신호와 연동

12. 프로덕션 패턴

Graceful Shutdown

std::atomic<bool> g_running{true};
std::signal(SIGINT,  { g_running = false; });

// do_accept 내부에서
if (!g_running) {
    work.reset();
    return;
}

구현 체크리스트

  • async_accept 완료 핸들러에서 재등록
  • 세션은 make_shared로 생성
  • 멀티스레드 시 공유 자원은 strand 또는 mutex로 보호
  • 모든 핸들러에서 error_code 확인
  • work_guard 사용 시 shutdown에서 reset() 호출
  • 소켓/버퍼 수명: shared_ptr 또는 람다 캡처로 유지

성능 비교

구성처리량 (req/s)CPU 사용률메모리
단일 스레드10,000100% (1 core)낮음
멀티스레드 (4개)35,00090% (4 cores)중간
멀티스레드 (8개)40,00080% (8 cores)높음

결론: 스레드 수는 CPU 코어 수와 비슷하게 설정하는 것이 최적입니다.


정리

항목설명
run()모든 작업 완료까지 블로킹
work_guardrun()이 종료되지 않게 유지
post작업을 큐에 넣어 나중에 실행
dispatch가능하면 즉시 실행
strand순차 실행 보장, mutex 대체
멀티스레드여러 스레드가 같은 io_context 처리
코루틴co_await로 콜백 지옥 회피
체이닝완료 핸들러에서 다음 작업 등록

핵심 원칙:

  1. 서버는 async_accept 재등록으로 유지
  2. 멀티스레드는 CPU 코어 수만큼
  3. 공유 자원은 strand로 보호
  4. post는 스레드 안전, dispatch는 성능 최적화
  5. 세션은 shared_ptr + enable_shared_from_this로 수명 관리

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 고성능 네트워크 서버, 비동기 I/O 시스템, 이벤트 기반 애플리케이션 등에서 필수입니다. 특히 수천 개의 동시 연결을 처리하는 서버에서 이벤트 루프 패턴은 필수적입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreferenceBoost.Asio 공식 문서를 참고하세요. C++20 코루틴은 Boost.Asio C++20 Coroutines를 참고하면 좋습니다.

Q. run()과 poll()의 차이는?

A. run()은 작업이 완료될 때까지 블로킹하지만, poll()은 즉시 준비된 작업만 처리하고 반환합니다. 게임 루프처럼 매 프레임마다 이벤트를 처리해야 하는 경우 poll()을 사용합니다.

Q. 스레드를 몇 개 만들어야 하나요?

A. 일반적으로 CPU 코어 수만큼 만드는 것이 최적입니다. std::thread::hardware_concurrency()로 확인할 수 있습니다. I/O 대기가 많으면 코어 수보다 약간 더 많이 만들 수 있습니다.

Q. 코루틴 vs 콜백, 어떤 것을 써야 하나요?

A. C++20을 사용할 수 있다면 코루틴이 가독성과 유지보수에 유리합니다. 레거시 환경이거나 팀이 코루틴에 익숙하지 않다면 콜백 + shared_from_this 패턴이 안정적입니다.

한 줄 요약: run·post·work_guard·strand·코루틴으로 고성능 비동기 이벤트 루프를 구현할 수 있습니다.

다음 글: [C++ 실전 가이드 #29-3] 멀티스레드 네트워크 서버: io_context 풀과 strand

이전 글: [C++ 실전 가이드 #29-1] Asio 입문: 비동기 I/O의 시작


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Boost.Asio 입문 | io_context·async_read
  • C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
  • C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

C++, Asio, 이벤트루프, 비동기, run, post, work_guard, strand, 코루틴, 멀티스레드 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ Boost.Asio 입문 | io_context·async_read
  • C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
  • C++ HTTP 기초 완벽 가이드 | 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]
  • C++ WebSocket 완벽 가이드 | Beast 핸드셰이크·프레임·Ping/Pong [#30-1]
  • C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]