IOCP(Input/Output Completion Port)
IOCP는 다수의 클라이언트의 요청을 효율적으로 처리하기 위한 방법들 중 하나이다.
사용할 스레드를 미리 만들어두고 소켓에서의 IO가 완료되었을 때 잠들어있는 스레드를 깨워서 작업을 처리한다.
최적화 할 수 있는 스레드의 수는 프로세서의 2배 정도가 적당하다고 한다.
IOCP는 스레드를 관리하기 위해 1개의 스레드 큐와 2개의 스레드 리스트, 1개의 작업 큐를 사용하고 각 스레드는 3개의 큐중 하나의 큐에만 존재할 수 있다.
IOCompletionQueue
FIFO Queue. 입출력이 완료된 작업들이 들어간다. 스레드들은 이 큐에서 작업을 꺼내서 수행한다.
WatingThreadQueue
LIFO Queue. 작업 대기중인 스레드들이 들어있다. 만약 입출력이 완료되었다면 이 큐에서 스레드를 하나 꺼내서 사용한다.
ReleaseThreadList
현재 작업을 수행하고 있는 스레드들이 들어간다. 만약 실행중인 스레드가 일시정지해야 한다면 해당 스레드를 PausedThreadList로 보내고 WatingThreadQueue에서 새로운 스레드를 하나 꺼내온다. 이것 때문에 프로세서의 갯수보다 많은 스레드를 미리 만들어 놓는 것이다.
PausedThreadList
어떠한 원인으로 인해 일시정지된 스레드들이 들어간다. 일시정지가 풀리더라도 ReleaseThreadList가 꽉 차있다면 바로 ReleaseThreadList로 보내지 않고 대기한다.
IOCP를 사용하려면 먼저 IOCP를 만들어야 한다.
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE fileHandle, // IOCP와 연결할 핸들. 최초 생성시 INVALID_HANDLE_VALUE를 넘긴다
_In_opt_ HANDLE ExistingCompletionPort, // IOCP 핸들. 최초 생성시 NULL
_In_ ULONG_PTR CompletionKey, // IO 완료 시 넘어갈 값. 사용자가 넘기고 싶은 값을 넘김
_In_ DWORD NumberOfConcurrentThreads) // 한 번에 동작할 수 있는 최대 스레드 개수. 0을 넘기면 프로세서 숫자로 자동 지정됨
HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); // CP 최초 생성
HANDLE port = CreateIoCompletionPort(socket, hPort, (ULONG_PTR)session, 0); // 디바이스-CP 연결
CreateIoCompletionPort 함수는 2가지 일을 한다.
1. CP 생성
2. 디바이스와 CP 연결
하나의 함수로 서로 다른일을 하기 때문에 명확하게 다른 이름으로 래핑하여 사용하면 좋을 것 같다.
IOCompletionQueue
입출력 완료시 말고도 유저가 직접 PostQueuedCompletionStatus 함수를 호출해서 정보를 직접 큐에 집어넣을 수도 있다.
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberofByteTransferred,
_In_ ULONG_PTR dwCompletionKey,
_in_opt_ LPOVERLAPPED lpOverlapped)
전달한 정보들은 입출력이 완료되면 CompletionQueue에 저장되어 스레드가 자신을 처리해주기를 기다리게 된다.
WatingThreadQueue
GetQueuedCompletionStatus 함수로 스레드를 대기 상태로 만든다.
ReleaseThreadList에 공간이 있고, 완료된 입출력이 있는 경우에만 대기 상태가 해제된다.
ReleaseThreadList
입출력 장치 또는 PostQueuedCompletionStatus로 보낸 정보를 GetQueuedCompletionStatus 함수를 통해서 받아온다.
BOOL WINAPI GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberofBytes,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED *lpOverlapped,
_In_ DWORD dwMilliseconds)
입출력 처리를 하고 난 뒤에 재호출해야만 다시 대기 상태로 전환된다. 재호출하지 않으면 대기 상태로 전환되지 않기 때문에 주의해야 한다.
PausedThreadList
GetQueuedCompletionStatus를 호출하지 않더라도 스레드가 Blocking 상태에 빠지면 IOCP가 알아서 PauseThreadList에 집어넣는다.