IOCP(I/O Completion Port)
IOCP는 I/O Completion Port의 약어로, 윈도우에서 지원하는 비동기 입출력 통지 모델이다.
이 모델은 OVERLAPPED I/O(이하 오버랩)를 기반으로 하고 있지만 차이점으로는 완료통지 방식이 바뀌고,
멀티스레드를 통한 성능 향상이 있다.
이 글에서는 오버랩 모델과 '완료통지'라는 개념을 알고 있다는 것을 전제로 하기 때문에 먼저 오버랩을
공부하고 IOCP를 찾아보면 도움이 될 것이다.
오버랩 vs IOCP
오버랩
오버랩은 비동기 함수를 호출하고 작업이 완료되면 Alertable Wait 상태를 통해 완료통지를 얻어올 수 있다.
완료되면 이벤트 또는 Completion Routine이라는 콜백함수가 호출된다.
IOCP
IOCP은 기본적인 형식은 오버랩을 기반으로 하고 있어 비슷하다. 하지만 추가로 CP라는 것이 사용된다.
CP는 IO'CP'에서의 그 CP다. CP는 비동기 작업의 결과물이 쌓이는 큐 이다.
CP의 내용물은 GetQueuedCompletionStatus(GQCS)함수에서 얻어올 수 있다.
차이점
오버랩은 스레드가 작업이 완료되어 Alertable Wait 상태가 되기까지 오버헤드가 생기고, 이 상태를 활용하는
APC큐는 스레드마다 독립적으로 있어 멀티스레드 프로그래밍에서 단점으로 작용한다. 하지만, CP는
모든 스레드가 공유하기 때문에 멀티스레드 프로그래밍을 더 유연하게 할 수 있게 된다.
IOCP의 흐름
IOCP 완료통지의 기본적인 흐름은 위 패러디 만화와 같다.
비동기 함수를 호출하면 완료된 작업이 GQCS를 통해 검출이 된다.
하지만는 IOCP에는 여러가지 제약이 있다. 먼저 IOCP에서 비동기 함수의 완료를 얻기 위해서는
입출력 핸들(소켓 or 파일 핸들)이 CP에 등록되어 있어야 하고, 무슨 작업이 완료되었는지 알기 위해서
오버랩 구조체를 상속받아 처리해야 한다.
1. CP 핸들만들고 등록시키기
HANDLE CreateIoCompletionPort(HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads);
CP핸들을 만들고, CP에 등록시키는 역할은 위 함수를 통해 이루어진다. CP를 만들기 위해선 첫번째 인자에 INVALID_HANDLE_VALUE
를 넣고, 다른 모든 인자로 NULL
을 넣어주면 CP핸들이 리턴된다.
등록은 첫번째 인자에 파일이나 소켓을 넣고, 두번째 인자에 CP핸들을 넣어주고, 세번째에는 비동기 함수를
호출하는 주체(소켓 or 파일 핸들)를 알아내기 위해서 넣어주는 역할을 한다. 딱히 필요없으면 NULL
을 넣어준다.
그리고 나머지 인자도 NULL
을 넣어준다.
2. Worker Thread 만들기
IOCP는 멀티스레드에서 고성능을 내기위한 목적으로 등장했다. 그래서 멀티스레드로 돌리는 것이 보편적이다.
아까 비동기함수의 완료는 GQCS함수를 통해 이루어진다고 했다. 이 작업은 멀티스레드로 작동하게 되는데
이 작업을 수행해줄 스레드를 Worker Thread라고 한다.
이 Worker Thread는 winapi를 사용해 만들어도 되고, std::thread
로 생성해도 된다. 단, std::thread
로 만든다면
main의 끝에서 join해줘야 한다.
Worker Thread의 예시코드는 다음과 같다.
struct OverlappedEx : OVERLAPPED
{
...
}
void WorkerThread()
{
while(true)
{
DWORD transferredBytes = 0; // 전송/수신된 바이트 수
ULONG_PTR ck = NULL; // Completion Key - CP에 등록할 때 넣어줬던 인자
OverlappedEx* overlap = NULL; // 확장된 오버랩 구조체
if(GetQueuedCompletionStatus(_cp /* CP 핸들 */,
&transferredBytes,
(PULONG_PTR)&ck,
(OVERLAPPED**)&overlap,
INFINITE))
{
// 비동기 함수가 완료됨
}
}
}
int main()
{
...
}
3. 비동기 함수 호출하기
비동기 함수의 종류는 여러가지가 있지만, 이 글에선 소켓입출력을 예시로 들겠다.
char buf[256] = "Hello, World!";
WSABUF wsaBuf;
wsaBuf.buf = buf;
wsaBuf.len = 256;
OverlappedEx overlap;
DWORD sentBytes = 0;
if(SOCKET_ERROR == WSASend(_sock, &wsaBuf, 1, &sentBytes, NULL, &overlapped, NULL))
{
if(WSAGetLastError() != WSA_IO_PENDING)
{
// Error.
}
}
언뜻봐서는 이벤트 기반의 오버랩 모델로 보인다. 하지만 Alertable Wait상태가 되기 위한 함수가 존재하지 않는다.
IOCP에선 CP를 통해 완료통지가 이루어지기 때문이다. 그리고, 이 비동기 함수를 호출한 후 완료가 되면 아까 만든 Worker Thread에서 완료통지가 올 것이다.
마무리
단순하게 비동기 함수 끝나면 GQCS에 나온다! 라는 정도만 알아도 IOCP의 절반을 이해한 것이다.
이해가 안되었다면 한번 인터넷에서 예제 코드를 찾아보면서 실습해보자!
'network' 카테고리의 다른 글
[Network] HTTP 프로토콜 분석 (0) | 2024.11.10 |
---|---|
[Network] 차세대 IO 모델 (0) | 2024.09.11 |
[Network] 02. IOCP 실습해보기 (1) | 2023.11.03 |