IOCP Core

Jaemyeong Lee·2025년 3월 9일

게임 서버1

목록 보기
141/220

IocpCore의 책임

한 줄 정의

IocpCore는 "IOCP 핸들 소유 + 등록(Register) + 완료 분배(Dispatch)"를 담당하는 엔진의 중심 클래스입니다.

왜 별도 클래스로 분리하는가

  • IOCP 핸들 생명주기를 한곳에서 관리할 수 있습니다.
  • Listener/Session은 "자기 로직"에 집중하고, 큐 처리 정책은 IocpCore에 모을 수 있습니다.
  • 테스트할 때도 Register/Dispatch 경로를 독립적으로 검증하기 쉽습니다.

최소 멤버 구성

class IocpCore
{
private:
    HANDLE _iocpHandle = nullptr; // Completion Port 핸들
};

생성/파괴와 생명주기

생성자 포인트

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;
    }
}

종료 순서 원칙

  1. 워커 루프 종료 신호 전달
  2. 워커 스레드 Join
  3. 마지막에 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)
    {
        // WAIT_TIMEOUT 또는 제어 경로
        return false;
    }

    IocpObject* object = reinterpret_cast<IocpObject*>(key);
    IocpEvent* event = reinterpret_cast<IocpEvent*>(ov);
    if (object == nullptr || event == nullptr)
        return false;

    // ok == FALSE && ov != nullptr 도 "실패 완료 이벤트"로 전달 가능
    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 핸들과 워커 스레드 정리 순서를 어떻게 설계할 것인가?

profile
李家네_공부방

0개의 댓글