Windows 운영체제에서 제공하는 비동기 입출력 모델 중 하나로, 고성능 서버 애플리케이션에서 효율적인 네트워크와 파일 입출력을 처리하기 위해 설계된 메커니즘이다.
Input, Output의 완료를 담당할 포트를 직접 지정해서 처리하겠다는 의미를 갖고 있다.
비유하자면 택배 분류 센터와 같다고 할 수 있다.
택배 분류 센터에서는 다음과 같은 단계로 작업이 진행될 것이다.
택배 요청 - 분류 센터에서 처리가 '완료된 요청만' 작업자가 가져가서 배달 - 작업자 수는 요청 개수에 맞춰 동적으로 조절
IOCP는 스레드 활성화 수를 동적으로 조절하여 요청에 맞는 스레드 수로 처리해, 자원을 효율적으로 관리하는 방법이라고 말할 수 있다.
IOCP의 원초적 목적은 '자원의 효율적 관리' 이다.
다만 이 효율적 관리가 어떤 과정을 내포하느냐는 I/O 작업에서 동시 수행되는 스레드 개수의 상한을 사용자가 직접 설정해, CPU 자원을 최대한 효율적으로 사용하는 것이다.
포인트는 스레드의 상한 개수를 사용자가 직접 설정한다는 것이다.
이 스레드는 작업을 처리하는데 필요한 요소라고는 알고 있을 텐데, 많으면 많을 수록 좋다고 생각할 수 있다.
이 경우를 생각해보자.
스레드를 대량으로 생성할 경우, CPU 컨텍스트 스위칭(스레드 전환 과정) 오버헤드가 증가하고, CPU가 불필요하게 점유되는 상황이 늘어난다.
여기서 오버헤드란, 어떤 처리를 하기 위해 소요되는 시간/메모리 등을 말하는데, 이 스레드 전환 과정인 컨텍스트 스위칭이 이루어지는 과정에서 많은 처리를 요구한다.
따라서 스레드가 무작정 많다고 꼭 좋은 것만은 아니라는 것이다.
이러한 이유로, 스레드 개수의 상한을 설정해 CPU 코어와 작업량에 맞춰 스레드가 동작하도록 구성하는 것이다.
일반적으로 '사용자의 CPU 코어 개수 * 2' 를 권장한다.
이렇게 만들어진 스레드들을 통해 내부적으로 '스레드 풀'을 사용하여 관리한다.
이 스레드 풀이란 여러 개의 스레드를 '대기' 상태로 미리 생성해놓고, 필요한 만큼 꺼내어 사용, 이후 완료 시 다시 스레드 풀이 넣어주는 과정이다.
이 과정에서 스레드는 필요 시 생성되고 작업 완료 시 파괴되는 동작이 아니라 필요 시 활성화/완료 시 대기 로의 상태 전환 동작을 하게 된다.
이를 통해 CPU의 소모를 감소시키고, 실행 상태의 스레드를 상황에 맞춰 조절해 잦은 컨텍스트 스위칭을 피할 수 있다.
ICOP 모델의 입출력 핸들의 등록, 비동기 I/O 요청, I/O의 완료통지 방법에 대한 그림이다.
하나씩 풀어보자.
이 함수를 통해 입출력 핸들, 완료 키(Completion Key)를 CP객체에 등록한다.
CP객체를 생성할 때도 동일한 함수를 사용한다.
CP객체란? IOCP객체를 의미하며 비동기 입출력을 관리하는 구조체이다.
이후 커널 내부에서 등록한 입출력 핸들과 완료 키를 관리해주는 Device List 자료구조에 추가한다.
Device List 는 등록한 입출력 핸들을 관리하는 객체이다.
IOCP Queue(IO 완료 큐 라고도 한다.) 라는 자료구조의 추가 과정이 일어난다.
ICOP 모델은 완료된 작업 결과를 IOCP Queue 에 넣은 후, CP객체를 이용해 사용자에게 알린다.
커널은 '스레드 풀링 매커니즘'을 이용해 IOCP Queue 에서 완료된 Overlapped I/O 작업을 가져와 후처리를 진행한다.
I/O 에 대한 처리를 Device Driver에 권한을 념겨, 별도의 스레드 없이 비동기로 둘 이상의 데이터 전송을 중첩시키는 것을 말한다.
Device Dirver는 작업을 끝내면 유저 버퍼에 데이터를 채워넣어 I/O에 대한 처리를 중첩해서 처리할 수 있고, 이를 통해 자원의 효율적 사용이 가능해진다.
위에서 '스레드 풀'에 대한 개념은 언급했다. 여기서 알아볼 것은 이 풀링 메커니즘이 어떤 과정으로 이뤄지는지 보려고 한다.
먼저 IOCP Queue에 완료된 작업 결과가 할당된다.
이후 WaitingThread Queue에 대기하고 있던 스레드 중 가장 나중에 추가된 몇개를 실행 상태로 전환(LIFO 구조)한 후, WaitingThread Queue에서 삭제 및 ReleaseThread List에 추가한다.
실행 상태 스레드는 IOCP Queue에서 완료된 작업 결과를 가져와 유저모드의 GetQueuedCompletionStatus()(이하 GQCS)함수로 반환한 다음 후처리를 진행한다.
이 후처리를 진행하던 중 대기작업을 하는 함수가 호출된다면 작업을 멈추고 해당 스레드는 대기 상태로 전환, PausedThread List 에 추가된다.
이후에 다시 실행 상태가 되면 스레드는 PausedThread List에서 나와 ReleaseThread List에 추가되고 전에 하던 작업을 이어서 마저 진행한다.
모든 작업을 마친 후 GQCS 함수를 호출하고, IOCP Queue에 완료된 작업이 있다면 여태까지의 과정을 반복하고 작업이 없다면 대기 상태로 전환 후 WaitingThread Queue에 추가된다.
정리하자면 GQCS에 진입한 스레드들은 IOCP Queue에 완료된 작업이 있을 때까지 대기하고, 완료된 작업이 있으면 Relase 에서 일을 처리한다. 처리 도중 대기 함수를 호출하면 Paused 로 들어갔다가 다시 활성화되면 하던 일을 마무리하고 이후 Waiting 에 들어가는 것이다.
풀링 기법에 맞게 전체적인 스레드 수는 고정이되 그 안에서 대기/활성 의 반복 과정이 이루어지며 작업을 처리하는 것이라고 짧게 설명할 수 있겠다.
이 과정 안에서 알아두어야 할 포인트로 WaitingThread Queue의 순서는 LIFO(Last In First Out) 라는 것이다.
이 이유로는 '컨텍스트 스위칭을 피하기 위해서' 가 있는데, 먼저 컨텍스트 스위칭은 서로 다른 스레드 간의 전환이 일어나는 것이라고 앞서 설명한 바 있다.
어떤 스레드가 일을 처리하던 중 대기 상태에 들어가고 다른 스레드가 실행 상태로 바뀌어서 이 일을 처리하게 한다면 이 스레드를 바꾸는 과정에서 컨텍스트 스위칭이 발생한다.
이 컨텍스트 스위칭은 결국 CPU의 연산을 소모하게 만들기 때문에 지양하는 방법인 것이다.
이를 해결하기 위해 선택한 것이 LIFO 구조로 WaitingThread Queue에서 방금 막 대기 상태로 들어간 스레드가 일감이 들어오면 바로 처리하도록 하여 상태 전환의 과정을 단축해 CPU를 최대로 활용하도록 하였다.
또 한가지 알아야 할 점은 '동시에 실행 상태에 있는 스레드의 개수를 커널이 조절하고 있다는 것을 명심'해야 한다는 것이다.
예를 들어 총 스레드 개수 중 사용자가 2개의 스레드를 활성 상태 유지하도록 설정했다고 해보자.
1개 스레드가 GQCS를 호출해 대기 상태에 진입한다면? 당연히 실행 중인 스레드는 현재 1개가 될 것이다.
그렇다면 커널은 2개의 활성 스레드를 유지해야 하므로 WaitringThread Queue에서 스레드 1개를 호출해 2개의 스레드를 유지할 것이다.
이때 대기 상태에 들어간 스레드가 대기 작업을 마치고 다시 실행된다면? 현재 실행 중인 스레드는 총 3개가 되어 설정값인 2개를 초과하게 된다.
이런 상황이 될 경우 커널은 먼저 후처리를 마치고 GQCS를 호출한 스레드를 다시 대기상태로 전환시켜 최대 2개를 유지할 수 있도록 한다.
이렇게 동시에 실행 상태에 있는 스레드 개수를 사용자가 설정한 값으로 유지될 수 있도록 '커널'이 조절하고 있다는 뜻이다.
마지막으로는 'WaitingThread Queue에 대기중인 스레드가 충분해야 한다'는 점이 있다.
스레드가 후처리 중 대기 작업 함수 GQCS를 호출하면 ReleaseThread 에서 삭제 후 PausedThread에서 대기할 것이다.
그렇다면 커널은 다시 실행 중인 스레드 개수를 유지하기 위해 WaitingThread 에서 스레드 하나를 실행 상태로 변환한다.
근데 이때 WaittingThread 에 대기 중인 스레드가 없다면? 사용자가 설정한 동시 실행 스레드 수를 유지하지 못하는 상황이 일어나고, 이 상황에서 더 최악으로 간다면 모든 스레드가 GQCS를 호출해 실행 중인 스레드가 없을 수도 있다.
이러한 상황이 일어나 연산 자체가 멈추는 것을 막기 위해 사전에 충분한 개수를 WaitingThread 에 대기시켜놔야 한다는 것이다.
이 충분한 개수는 위에서는 2n(n은 CPU 코어 수) 라고 언급했는데, 2n+1의 수가 권장되는 경우도 있다.
1. 2n + 1 : 입출력 작업량이 많은 어플리케이션, CPU 코어 수가 낮은 사용자 환경
2. 2n : CPU 사용률이 높은 작업, 입출력 작업이 상대적으로 적고 계산 작업이 많은 어플리케이션
다음과 같이 나눠진다.
마지막으로 IOCP의 주요 특징에 대해 살펴보면서 글을 마무리한다.
1. 커널 레벨의 큐 : Windows 커널에서 완료된 I/O 작업을 관리하는 큐를 제공한다. (I/O 완료 큐)
이를 통해 동기화 및 작업 분배의 효율을 극대화 시킬 수 있다.
2. 스레드 풀링 : 대기 상태의 스레드를 생성한다.
생성/파괴의 동작이 아니라 대기/활성화 의 반복을 통해 적은 스레드 수로 많은 I/O 요청을 처리한다.
3. 높은 성능
CPU/메모리 자원을 효율적으로 사용해 여러 클라이언트 처리에서 안정적인 성능을 보장할 수 있다.
https://marmelo12.tistory.com/265
[온라인 서버] I/O Completion Port(IOCP) 모델 이론
I/O Completion Port 모델 - 윈도우즈에서 제공하는 I/O모델 중 최고의 성능 - Completion Port객체는 Overlapped I/O에서 쓰레드 풀링과 Queue라는 메커니즘을 동시에 접목. 쓰레드 풀링 - 풀(Pool)이란 집합소 또
marmelo12.tistory.com
https://github.com/jacking75/edu_cpp_IOCP?tab=readme-ov-file
GitHub - jacking75/edu_cpp_IOCP: IOCP 실습
IOCP 실습. Contribute to jacking75/edu_cpp_IOCP development by creating an account on GitHub.
github.com
https://sanghun219.tistory.com/104
상상면접 : IOCP란 ? (Socket)
면접관 : IOCP에 대해서 설명해주시겠어요? 나 : 먼저, IOCP의 목적은 동시에 수행되는 스레드의 상한을 설정해서 CPU의 자원을 최대한 효율적으로 사용하게 하는 것입니다. 나 : IOCP는 Overlapped I/O가
sanghun219.tistory.com
전송 계층(Transport Layer) (3) | 2024.12.20 |
---|