boost.asio의 io_context 파헤치기
복잡한 비동기 프로그래밍에서 io_context는 비동기 IO들을 연결해 주는 중계소이자 엔진 역할을 한다.

io_context는 Boost.Asio 라이브러리 사용시 비동기 I/O 작업 결과들을 모아서 처리해주는 녀석이다.
io_context가 뭘 하는 녀석인데?
전달받은 여러 비동기 작업(예: 네트워크, 타이머 등)을 모아서 처리하는 녀석이다. 만약 이 녀석이 없으면 네트워크에 보낸 데이터가 왔는지 매번 '왔니? 왔어' 하고 확인해야 하는 귀차니즘이 발생하겠지. 그리고 비동기 작업이 생기면 적절한 시점에 작업을 처리 할 함수가 호출되게 된다. 또 한 개 또는 여러 개의 스레드에서 분리해서 작업을 처리 할 수도 있다.
- 비동기 작업 스케줄링
- 완료 이벤트 처리
- 타이머 관리
- 네트워크 I/O 관리

사용법
단일 스레드
#include <boost/asio.hpp>
#include <iostream>
int main() {
boost::asio::io_context io_context;
// 작업 추가
io_context.post([]() {
std::cout << "안녕하세요!\n";
});
io_context.post([]() {
std::cout << "io_context입니다!\n";
});
// 작업 실행. 모든 일이 끝날 때까지 여기서 블로킹(대기) 상태가 된다.
io_context.run();
return 0;
}

멀티스레드
#include <boost/asio.hpp>
#include <thread>
#include <iostream>
#include <chrono>
int main() {
// 여기서 io_context 객체는 "메인 스레드"에서 생성된다.
// 사실상 io_context는 내부적으로 비동기 작업(핸들러)들을 담아두는 task queue(작업 큐)와 비슷한 구조체라고 생각하면 편하다.
boost::asio::io_context io_context; // 메인 스레드에 생성됨 (비동기 작업 큐 역할)
// 이벤트 루프를 실행하는 worker 스레드를 생성한다.
std::thread worker([&io_context]() {
std::cout << "[Worker] run() 시작\n";
// run()을 호출하면 현재 스레드는 io_context 내부 task queue에서 작업이 들어오길 기다리며 블로킹된다.
io_context.run(); // worker 스레드는 이 부분에서 블로킹됨(이벤트 루프 대기)
std::cout << "[Worker] run() 종료\n";
});
// 메인 스레드는 별도 작업 수행 후, io_context에 작업(post) 추가
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "[Main] 작업 post\n";
io_context.post([]() {
std::cout << "[Worker] 비동기 작업 실행\n";
});
std::this_thread::sleep_for(std::chrono::seconds(1));
io_context.stop(); // 메인 스레드에서 이벤트 루프 정지 신호
worker.join();
return 0;
}

메서드
run()
- 이벤트 루프를 실행하고 모든 작업이 완료될 때까지 블로킹
- 더 이상 처리할 작업이 없으면 반환
io.run(); // 모든 작업 완료까지 실행
run_one()
- 최대 하나의 핸들러만 실행하고 반환
- 논블로킹 방식으로 작업을 처리할 때 유용
io.run_one(); // 하나의 핸들러만 실행
poll()
- 준비된 핸들러를 모두 실행하지만 블로킹하지 않음
- 즉시 반환
io.poll(); // 준비된 핸들러 모두 실행 (논블로킹)
poll_one()
- 준비된 핸들러 중 최대 하나만 실행하고 즉시 반환
io.poll_one(); // 하나의 핸들러만 실행 (논블로킹)
stop()
- 이벤트 루프를 중지
- 이후
run()호출은 즉시 반환
io.stop(); // 이벤트 루프 중지
reset()
stop()후 다시 사용하기 위해 상태를 리셋
io.stop();
io.reset(); // 상태 리셋
io.run(); // 다시 실행 가능
work_guard 사용
io_context에 처리할 일이 없는 경우에 run은 종료된다. 앞서 예제처럼 스레드에 만들어 놓은경우 스레드가 종료 되겠지. 그런데 처리할 일이 없이 대기해야 하는 경우가 있잖아? 예를 들면 버튼을 누를 때까지 대기하는거 같은거. 이 때 work_guard는 io_context에 아직 처리할 작업이 없어도, 실행이 멈추지 않게 지켜주는 역할을 한다. 즉, 작업이 추가될 때까지 io_context가 계속 돌아가게 해준다.
#include <boost/asio.hpp>
#include <thread>
int main() {
boost::asio::io_context io;
// work_guard로 io_context 유지
// work_guard는 io_context에 "아직 할 일이 있다"는 표식을 남김으로써
// io_context::run()이 즉시 종료되는 것을 방지해준다.
// 원리는, work_guard가 살아 있는 동안에는 내부적으로 io_context의 작업 카운터가 1 증가해 있어
// 아무 작업이 없어도 run()이 블록 상태로 계속 대기하게 된다.
auto work = boost::asio::make_work_guard(io);
std::thread t([&io]() {
io.run(); // work_guard가 있으면 계속 실행
});
// 나중에 작업 추가 가능
boost::asio::steady_timer timer(io, boost::asio::chrono::seconds(1));
timer.async_wait([](boost::system::error_code ec) {
std::cout << "작업 완료" << std::endl;
});
// work_guard 해제하면 io.run()이 종료됨
work.reset();
t.join();
return 0;
}

io_context, post, run 각각을 멀티 스레드로 만들면?
이게... 아주 권장되는(Best Practice) 패턴 중 하나라고 하네. 👍
이 구조가 고성능 서버나 멀티쓰레드 프로그램을 만들 때 사용하는 **"생산자-소비자(Producer-Consumer) 패턴"**이자 "쓰레드 풀(Thread Pool) 패턴".
1. 역할 분담 (식당 비유) 🍽️
이 구조를 식당에 비유하면.
- 생성 스레드 (사장님): 식당(
io_context)을 세우고 문을 연다. - Post 스레드 (웨이터): 손님에게 주문(작업)을 받아서 주방 주문서(
Queue)에 계속 붙인다. 요리는 안 하고. - Run 스레드 (요리사들): 주문서를 보고 요리(작업 처리)만 한다. 주문을 누가 받았는지는 관심 없고, 그냥 들어온 순서대로 미친 듯이 만든다.
이 셋이 완전히 분리되어 있으면, 웨이터는 주문받느라 바쁘고, 요리사는 요리하느라 바쁠 때 서로 방해하지 않고 각자 최대 속도를 낼 수 있게 되는거다.
2. 그래서 뭐가 좋은데?
① 메인 로직의 반응성 극대화 (Non-blocking)
post를 하는 스레드(주로 메인 UI 스레드나 네트워크 수신 스레드)는 작업을 직접 처리하지 않고 "던져두기만" 한다.
post 함수는 거의 즉시 리턴되기 때문에, 메인 스레드는 멈추지 않고(Non-blocking) 계속해서 다음 요청을 빠르게 받아낼 수 있다.
② 멀티코어 성능 100% 활용 (Parallelism)
만약 run()을 호출하는 스레드를 4개 만들었다고 해보자. (요리사 4명)
그러면 io_context에 쌓인 일감들을 4개의 스레드가 동시에 가져가서 처리하게 되는거다. CPU 코어가 4개라면 4배의 속도로 일을 처리할 수 있는 진정한 병렬 처리가 가능해짐.
③ 자동 로드 밸런싱 (Load Balancing)
요리사 A가 어려운 요리(오래 걸리는 작업)를 하고 있다면, 놀고 있는 요리사 B와 C가 알아서 다음 주문을 가져간다.
개발자가 "너는 이거 해, 넌 저거 해"라고 지정해 줄 필요 없이, io_context가 알아서 노는 스레드에게 일을 분배해준다.
3. 코드 예시: 셋이 따로 노는 완벽한 분업
이 코드는 1개의 Post 스레드와 **3개의 Run 스레드(쓰레드 풀)**가 협업하는 구조이다.
#include <iostream>
#include <boost/asio.hpp>
#include <thread>
#include <vector>
using namespace std;
boost::asio::io_context io;
// [Post 스레드] 웨이터 역할
void producer() {
for (int i = 0; i < 5; ++i) {
// 작업을 던지기만 함 (누가 처리할지는 모름)
io.post([i]() {
// 현재 이 작업을 실행하는 스레드의 ID 출력
cout << "작업 " << i << " 처리 중... (Thread: "
<< this_thread::get_id() << ")\n";
this_thread::sleep_for(chrono::milliseconds(100)); // 0.1초 걸림
});
cout << "[Producer] 작업 " << i << " 등록 완료\n";
this_thread::sleep_for(chrono::milliseconds(50));
}
}
int main() {
// 1. [생성 스레드] io_context는 전역(또는 메인)에 존재
// work_guard: 할 일이 없어도 run 스레드가 퇴근하지 못하게 잡는 역할
// (이게 없으면 producer가 post하기도 전에 run 스레드들이 종료될 수 있음)
auto work_guard = boost::asio::make_work_guard(io);
// 2. [Run 스레드들] 요리사 3명 고용 (쓰레드 풀)
vector<thread> threadGroup;
for (int i = 0; i < 3; ++i) {
threadGroup.emplace_back([]() {
io.run(); // 여기서 대기하다가 post되면 낚아채서 실행!
});
}
// 3. [Post 스레드] 웨이터 1명 고용 (별도 스레드에서 post)
thread poster(producer);
// (메인 스레드는 여기서 감독하거나 딴짓 가능)
poster.join(); // 웨이터 퇴근 기다림
// 더 이상 일이 없으니 요리사들도 퇴근 준비
work_guard.reset(); // work_guard 해제 -> 일이 다 떨어지면 run 종료
for (auto& t : threadGroup) {
t.join(); // 요리사들 퇴근 기다림
}
cout << "모든 작업 종료!\n";
return 0;
}
⚠️ 주의할 점: "동시 접근의 위험" (strand)
run()을 하는 스레드가 여러 개라면, 하나의 변수(데이터)에 여러 스레드가 동시에 접근하는 상황이 발생할 수 있다. (mutex가 필요한 상황!)
Boost.Asio에서는 이럴 때 mutex를 직접 쓰는 대신 **boost::asio::strand**라는 아주 우아한 도구를 쓴다.
strand.post(...): "이 작업들은 무조건 줄 세워서 한 번에 하나씩만 실행해 줘." (순서 보장, 동시 접근 방지)run스레드가 100개라도,strand로 묶인 작업들은 절대 동시에 실행되지 않고 안전하게 처리된다.