24. IOCP(I/O Completion Port)와 워커 스레드 풀의 내부 동작
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C++
// 문제 24 — IOCP(I/O Completion Port)와 워커 스레드 풀의 내부 동작
//
// 실제 Windows IOCP API(CreateIoCompletionPort, GetQueuedCompletionStatus 등)를
// 그대로 쓰면 리눅스에서 컴파일이 안 되므로, 아래 코드는 IOCP의 핵심 동작을
// 이식 가능한 C++ 표준 라이브러리만으로 "시뮬레이션"한 것이다.
// 실제 IOCP를 다뤄본 적이 없어도, 아래 시뮬레이션 코드의 동작을 근거로
// 실제 Windows IOCP가 왜 이렇게 설계되었는지 설명할 수 있어야 한다.
//
// 아래 코드에서:
// - CompletionPort::post() 는 실제 IOCP의 "완료 통지가 완료 큐에 쌓이는 것"에 대응한다.
// - CompletionPort::getNext() 는 GetQueuedCompletionStatus() 호출에 대응한다.
// - concurrencyHint 는 CreateIoCompletionPort()의 마지막 인자(NumberOfConcurrentThreads)에 대응한다.
// - runningWorkers_ 카운터로 "현재 실제로 실행 중인(대기 상태가 아닌) 워커 수"를 시뮬레이션한다.
//
// (A) 워커 스레드 개수(NUM_WORKER_THREADS)와 concurrencyHint 값이 다르게 설정되어 있다.
// (B) OverlappedContext 안에 "다음 단계에서 다시 post 할 체이닝 콜백"이 들어있다.
//
// 과제:
// 1. 아래 코드를 컴파일 관점에서만 보고(직접 실행 결과를 몰라도), 이 프로그램의
// 워커 스레드 수가 8개인데 concurrencyHint가 2로 설정된 상황에서 실제 Windows IOCP라면
// "동시에 실행 가능한 워커 스레드 수"가 어떻게 제한되는지 설명하라.
// 2. GetQueuedCompletionStatus (시뮬레이션에서는 getNext())가 큐가 비어 있을 때
// 워커 스레드를 어떻게 처리하는지, 그리고 완료 통지가 도착했을 때 "이미 실행 중인
// 스레드 수가 concurrencyHint 미만이면" 어떤 스레드가 깨어나는지 설명하라.
// 3. (B)의 체이닝 콜백처럼 하나의 OVERLAPPED 완료 처리 안에서 또 다른 비동기 I/O를
// 걸고 그 결과를 다시 완료 포트로 흘려보내는 패턴이 실제 게임 서버 네트워크 코드에서
// 왜 흔히 쓰이는지(recv 완료 → 다음 recv 재요청 등) 설명하라.
// 4. 워커 스레드 수를 concurrencyHint보다 훨씬 크게(cpu_count * 4 등) 잡는 이유와,
// 그럼에도 실제 "동시 실행" 스레드 수는 왜 concurrencyHint에 가깝게 유지되는지 설명하라.
#include <atomic>
#include <condition_variable>
#include <cstdio>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
struct OverlappedContext {
int connectionId;
std::function<void(int)> onComplete; // 완료 시 실행할 콜백 (체이닝 가능)
};
class CompletionPort {
public:
explicit CompletionPort(int concurrencyHint)
: concurrencyHint_(concurrencyHint), runningWorkers_(0), stop_(false) {}
// 실제 PostQueuedCompletionStatus() 에 대응
void post(OverlappedContext ctx) {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push_back(std::move(ctx));
}
cv_.notify_one();
}
// 실제 GetQueuedCompletionStatus() 에 대응
bool getNext(OverlappedContext& out) {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || stop_; });
if (stop_ && queue_.empty()) return false;
out = std::move(queue_.front());
queue_.pop_front();
return true;
}
void shutdown() {
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
cv_.notify_all();
}
int concurrencyHint() const { return concurrencyHint_; }
std::atomic<int>& runningWorkers() { return runningWorkers_; }
private:
int concurrencyHint_;
std::atomic<int> runningWorkers_;
bool stop_;
std::mutex mutex_;
std::condition_variable cv_;
std::deque<OverlappedContext> queue_;
};
constexpr int NUM_WORKER_THREADS = 8; // (A) 워커 스레드 수
constexpr int CONCURRENCY_HINT = 2; // (A) IOCP concurrency value
void workerLoop(CompletionPort& iocp, int workerIndex) {
OverlappedContext ctx;
while (iocp.getNext(ctx)) {
iocp.runningWorkers().fetch_add(1);
std::printf("[worker %d] conn=%d 처리 시작 (running=%d)\n",
workerIndex, ctx.connectionId, iocp.runningWorkers().load());
if (ctx.onComplete) {
ctx.onComplete(ctx.connectionId); // (B) 체이닝: 다음 recv를 다시 post
}
iocp.runningWorkers().fetch_sub(1);
}
}
int main() {
CompletionPort iocp(CONCURRENCY_HINT);
std::vector<std::thread> workers;
for (int i = 0; i < NUM_WORKER_THREADS; ++i) {
workers.emplace_back(workerLoop, std::ref(iocp), i);
}
// 클라이언트 5개의 recv 완료를 흉내낸 초기 이벤트 post
for (int connId = 0; connId < 5; ++connId) {
iocp.post(OverlappedContext{
connId,
[&iocp](int cid) {
// recv 완료 처리 후 "다음 recv"를 다시 완료 포트에 걸어주는 체이닝
std::printf(" conn=%d 다음 recv 재요청\n", cid);
}
});
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
iocp.shutdown();
for (auto& w : workers) w.join();
return 0;
} 내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.