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: 핸들러 실행 순서 혼란
post와 dispatch의 차이를 모르고 사용하면, 핸들러가 예상과 다른 순서로 실행될 수 있습니다. dispatch는 현재 핸들러 내부에서 즉시 실행되므로 재귀 깊이에 주의해야 합니다.
시나리오 5: run() 호출 후 io_context 재사용
io.run()이 반환된 io_context는 “stopped” 상태입니다. io.restart()를 호출하지 않고 다시 run()을 호출하면 아무 작업도 실행되지 않습니다.
해결책:
- work_guard: “아직 할 일이 있다”고 표시
- 완료 핸들러에서 재등록:
async_accept완료 시 다시async_accept등록 - 멀티스레드 run(): 여러 스레드가 동시에 이벤트 처리
목표:
run()/run_one()/poll()동작 이해post/dispatch로 작업 큐 관리work_guard로 서버 유지- 멀티스레드 이벤트 루프 구현
- 완료 핸들러 체이닝 패턴
요구 환경: Boost.Asio 1.70 이상
이 글을 읽으면:
- 이벤트 루프의 동작 원리를 이해할 수 있습니다.
- 서버가 종료되지 않게 유지할 수 있습니다.
- 멀티스레드로 성능을 향상시킬 수 있습니다.
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 이벤트 루프 동작 원리
- run/run_one/poll 비교
- work_guard로 서버 유지
- post와 dispatch
- 멀티스레드 이벤트 루프
- strand 완전 예제
- C++20 코루틴
- 완료 핸들러 체이닝
- 실전 예시
- 일반적인 에러와 해결법
- 모범 사례
- 프로덕션 패턴
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
이벤트 루프의 핵심:
- 등록된 비동기 작업을 확인
- I/O 이벤트 발생 대기 (epoll/kqueue/IOCP)
- 이벤트 발생 시 완료 핸들러 실행
- 1번으로 돌아가서 반복
- 더 이상 작업이 없으면 종료
시퀀스 다이어그램: 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. 모범 사례
- shared_from_this: 세션 클래스는
enable_shared_from_this상속,make_shared로 생성 - strand: 멀티스레드
run()사용 시 공유 상태는 전용strand로 보호 - 에러 코드: 모든 비동기 핸들러에서
error_code확인 - post vs dispatch: 다른 스레드 →
post, 핸들러 내부 →dispatch(재귀 주의) - 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,000 | 100% (1 core) | 낮음 |
| 멀티스레드 (4개) | 35,000 | 90% (4 cores) | 중간 |
| 멀티스레드 (8개) | 40,000 | 80% (8 cores) | 높음 |
결론: 스레드 수는 CPU 코어 수와 비슷하게 설정하는 것이 최적입니다.
정리
| 항목 | 설명 |
|---|---|
| run() | 모든 작업 완료까지 블로킹 |
| work_guard | run()이 종료되지 않게 유지 |
| post | 작업을 큐에 넣어 나중에 실행 |
| dispatch | 가능하면 즉시 실행 |
| strand | 순차 실행 보장, mutex 대체 |
| 멀티스레드 | 여러 스레드가 같은 io_context 처리 |
| 코루틴 | co_await로 콜백 지옥 회피 |
| 체이닝 | 완료 핸들러에서 다음 작업 등록 |
핵심 원칙:
- 서버는
async_accept재등록으로 유지 - 멀티스레드는 CPU 코어 수만큼
- 공유 자원은
strand로 보호 post는 스레드 안전,dispatch는 성능 최적화- 세션은
shared_ptr+enable_shared_from_this로 수명 관리
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 고성능 네트워크 서버, 비동기 I/O 시스템, 이벤트 기반 애플리케이션 등에서 필수입니다. 특히 수천 개의 동시 연결을 처리하는 서버에서 이벤트 루프 패턴은 필수적입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 Boost.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]