IocpCore의 책임
한 줄 정의
IocpCore는 "IOCP 핸들 소유 + 등록(Register) + 완료 분배(Dispatch)"를 담당하는 엔진의 중심 클래스입니다.
왜 별도 클래스로 분리하는가
- IOCP 핸들 생명주기를 한곳에서 관리할 수 있습니다.
- Listener/Session은 "자기 로직"에 집중하고, 큐 처리 정책은
IocpCore에 모을 수 있습니다.
- 테스트할 때도 Register/Dispatch 경로를 독립적으로 검증하기 쉽습니다.
최소 멤버 구성
class IocpCore
{
private:
HANDLE _iocpHandle = nullptr;
};
생성/파괴와 생명주기
생성자 포인트
IocpCore::IocpCore()
{
_iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
assert(_iocpHandle != nullptr);
}
CreateIoCompletionPort 실패값은 nullptr입니다.
- 따라서 실패 체크는
INVALID_HANDLE_VALUE가 아니라 nullptr 기준이 맞습니다.
소멸자 포인트
IocpCore::~IocpCore()
{
if (_iocpHandle != nullptr)
{
::CloseHandle(_iocpHandle);
_iocpHandle = nullptr;
}
}
종료 순서 원칙
- 워커 루프 종료 신호 전달
- 워커 스레드
Join
- 마지막에 IOCP 핸들 정리
- 핸들을 먼저 닫으면 워커가 예기치 않은 실패 경로로 빠질 수 있습니다.
Register 함수 (연결 단계)
역할
IocpObject의 핸들(소켓)을 IOCP에 연결합니다.
- Completion Key로 객체 식별자를 넣어, 완료 시 해당 객체를 복원합니다.
구현 예시
bool IocpCore::Register(const IocpObjectRef& iocpObject)
{
HANDLE result = ::CreateIoCompletionPort(
reinterpret_cast<HANDLE>(iocpObject->GetHandle()),
_iocpHandle,
reinterpret_cast<ULONG_PTR>(iocpObject.get()),
0);
return (result == _iocpHandle);
}
Completion Key vs Overlapped
| 항목 | 용도 |
|---|
| Completion Key | 어떤 객체(Listener/Session)의 완료인지 식별 |
OVERLAPPED* | 어떤 작업(Recv/Send/Accept) 완료인지 식별 |
수명 주의
- Completion Key에 raw pointer를 넣는다면 객체 수명 보장이 필수입니다.
- 세션 해제보다 늦게 완료가 도착할 수 있으므로 참조 카운트 정책이 필요합니다.
Dispatch 함수 (완료 분배 단계)
핵심 역할
GetQueuedCompletionStatus로 완료를 꺼냅니다.
- 복원한 객체에
Dispatch(event, bytes)를 호출해 실제 처리로 넘깁니다.
구현 흐름 예시
bool IocpCore::Dispatch(uint32 timeoutMs)
{
DWORD bytes = 0;
ULONG_PTR key = 0;
OVERLAPPED* ov = nullptr;
BOOL ok = ::GetQueuedCompletionStatus(_iocpHandle, &bytes, &key, &ov, timeoutMs);
if (!ok && ov == nullptr)
{
return false;
}
IocpObject* object = reinterpret_cast<IocpObject*>(key);
IocpEvent* event = reinterpret_cast<IocpEvent*>(ov);
if (object == nullptr || event == nullptr)
return false;
object->Dispatch(event, static_cast<int32>(bytes));
return true;
}
GQCS 결과 해석
| 조건 | 의미 | 대응 |
|---|
ok == TRUE | 정상 완료 | 일반 처리 |
ok == FALSE && ov != nullptr | 실패 완료(오류 포함) | 오류 코드 포함해서 객체에 전달 |
ok == FALSE && ov == nullptr | 타임아웃/종료 제어 경로 | 루프 유지/종료 판단 |
워커 스레드 운용
기본 패턴
for (int32 i = 0; i < workerCount; i++)
{
GThreadManager->Launch([&]() {
while (GServerRunning)
GIocpCore.Dispatch(INFINITE);
});
}
워커 수 결정 가이드
- 시작값은
std::thread::hardware_concurrency() 근처에서 잡습니다.
- 실제 값은 완료 큐 지연, CPU 사용률, 컨텍스트 스위칭 지표로 조정합니다.
종료 신호
- 무한 대기(
INFINITE)를 쓰면, 종료 시 PostQueuedCompletionStatus로 워커를 깨우는 설계가 안전합니다.
흔한 실수와 예방 체크
| 실수 | 결과 | 예방 |
|---|
GQCS FALSE를 전부 같은 오류로 처리 | 타임아웃/실패 완료/종료 경로 혼동 | ov 유무로 분기 |
| Register는 성공했는데 수명 관리 미흡 | Completion Key 댕글링 | 참조 카운트 정책 고정 |
| 워커 종료 전에 IOCP 핸들 닫기 | 예기치 않은 실패/레이스 | 종료 순서 준수 |
| timeout 0 폴링 루프 | CPU 과점유 | INFINITE + 제어 이벤트 또는 적절한 timeout |
강의 시 유의사항
강조 포인트
- Part 2의 본질은 "API 호출법"이 아니라 분배기(Dispatcher) 설계입니다.
- Register와 Dispatch는 엔진의 뼈대이며, 이후 파트는 여기에 살을 붙이는 과정입니다.
- 실패 경로(
ok == FALSE)를 제대로 가르쳐야 실전에서 버그를 줄일 수 있습니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| Dispatch는 성공 완료만 다룬다 | 실패 완료도 객체로 전달해 정리해야 한다 |
| Completion Key만 있으면 충분하다 | 작업 타입 구분은 OVERLAPPED가 담당한다 |
| 워커 수를 많이 늘리면 무조건 빨라진다 | 락 경쟁/문맥 전환으로 오히려 느려질 수 있다 |
체크 질문 (스스로 답해보기)
- 왜
CreateIoCompletionPort 실패 체크를 nullptr로 해야 하는가?
ok == FALSE && ov != nullptr 경로를 객체 Dispatch로 넘겨야 하는 이유는 무엇인가?
- 서버 종료 시 IOCP 핸들과 워커 스레드 정리 순서를 어떻게 설계할 것인가?