CLR의 I/O 비동기 스레드 관련해 ‘제프리 리처의 CLR via C# 4판’ 에서 설명하는 내용을 정리해 봅니다. 프로세스 (Process)프로세스란 운영체제에서 자원을 할당 받아 독립적 메모리 공간을 갖는 엔티티로, 애플리케이션의 인스턴스 입니다. 스레드 (Thread)기본적인 개념으로 스레드는 프로세스가 할당 받은 자원을 이용해서 작업의 효율적인 병렬 실행을 가능하도록 합니다. 스레드는 하나의 스택(Stack)을 갖고 있고 스택은 스레드 마다 고유 합니다.
CPU 입장에서 스레드는
제프리 리처의 CLR via C# 4판, 26장. 스레드의 기본 내용 중
CPU를 가상화 하기 위한 운영체제의 개념입니다. 운영체제는 개별 프로세스에게 각자의 스레드를 나누어 주는데, 이를 통해 특정 응용프로그램이 무한 루프에 빠지게 되더라도 그 프로그램만 영향을 받고 다른 프로세스들은 계속 수행될 수 있도록 해줄 수 있습니다.
모든 스레드는 다음 정보를 갖고 있습니다.
- 스레드 커널 객체 : CPU 내의 레지스터들의 값을 저장
- 스레드 환경 블록(Thread environment block, TEB) : 유저 모드에 할당, 예외 처리 정보 저장
- 유저 모드 스택 : 지역 변수, 함수의 매개변수 저장, 다음으로 수행해야 할 위치 저장
- 커널 모드 스택 : 프로그램이 커널 모드 함수로 매개변수를 전달할때 사용
- DLL 스레드 attach/detach 통지 : 새로운 스레드 생성시 모든 비관리 DLL들이 DLL_THREAD_ATTACH 플래그를 매개변수로 호출, 반대로 스레드 종료시 DLL_THREAD_DETACH 플래그를 매개변수로 호출
CLR 스레드와 윈도우 스레드제프리 리처의 CLR via C# 4판, 26장. 스레드의 기본 내용 중
CLR 스레드는 윈도우의 스레드 기능을 사용하여 윈도우 스레드와 완전히 동일합니다. CLR 스레드 풀제프리 리처의 CLR via C# 4판, 27장. 계산 중심의 비동기 작업 내용 중
CLR 스레드 풀은 크게 워커 스레드와 I/O 스레드를 관리하고 있습니다. 일반적으로 작업자 스레드는 높은 사용률로 인해 커널 영역의 I/O 작업 콜백을 디스패치 하는 데 사용할 수 있는 스레드가 고갈되어 교착 상태가 발생할 수 있는 상황을 방지하기 위해 각각의 풀을 별도로 관리 합니다.
[ThreadPool의 Worker Thread / I/O Thread 설명 참고]
따라서 I/O 스레드가 바로 스레드 풀에 반환되도록 I/O 콜백을 처리할 때 최소한의 작업이 수행되도록 해야 합니다. 그렇지 않으면 CLR이 작업자 스레드로 I/O 완료 스레드 풀을 ‘하이재킹’하여 위에 설명된 교착 상태가 발생할 위험이 있습니다. (이 부분은 정확하지 않습니다.) IO Completion Port Thread제프리 리처의 CLR via C# 4판, 28장. I/O 중심의 비동기 작업 내용 중
I/O 작업에 있어서 비동기로 처리 할 수 있는데 다음은 파일 읽기 작업을 예를 들어 설명 합니다.
①. 유저 모드 코드에서 파일 IO 작업 수행을 하면 IRP(I/O Request Packet)라고 부르는 조그만 데이터 구조체를 할당 합니다. ②. IRP 구조체에는 파일의 핸들, 파일 내에서의 읽거나 쓸 위치, 읽어올 데이터를 저장할 Byte[] 주소, 데이터 크기 및 기타 관련 정보들이 있습니다. ③. IRP 데이터 구조체가 커널로 전달 됩니다. (유저 모드 -> 커널 모드) ④ 커널은 IRP 내의 디바이스 핸들을 이용하여 어느 디바이스로 요청이 전달 되어야 하는지를 확인한 후, 적절한 디바이스 드라이버의 IRP 큐에 큐잉 합니다. ⑤ 디바이스 드라이버는 IRP 정보를 회로 기관의 실제 하드웨어로 전달하고, 하드웨어 디바이스는 요청된 I/O 작업을 수행 합니다.
그런데 하드웨어 디바이스가 I/O 작업을 수행하는 동안 I/O 작업을 요청하였던 사용자 스레드는 아무런 할 일이 없기 때문에 운영체제는 스레드가 CPU 시간을 낭비하지 않도록 슬립 상태로 변경 합니다.
이 처리는 매우 비효율 적으로 하드웨어 디바이스가 I/O 작업을 마칠때 까지 UI 스레드가 블로킹 되버리는 문제가 발생 합니다. 그래서 I/O 를 비동기로 처리를 해야 하는데 비동기 처리 과정은 다음과 같습니다. ④ 번 하드웨어 디바이스 드라이버에 IRP를 큐잉는 과정까지는 동일하지만 IRP 큐잉 후 사용자 스레드를 블로킹하지 않고 바로 반환합니다.
그럼 언제, 어떻게 I/O 작업이 마친 데이터에 접근할 수 있는걸까요?
⑥ 하드웨어 디바이스가 IRP 요청을 완료하면 완료된 IRP를 스레드 풀에 큐잉 합니다. ⑦ 스레드 풀 내의 스레드는 완료된 IRP 스레드를 가져와서 예외를 설정하거나, 작업의 결과 값을 설정 합니다. ⑧ 이후 사용자 코드를 수행하여 안전하게 Byte[] 내부에 있는 데이터에 접근 할 수 있습니다.
[윈도우 운영체게가 비동기 I/O 작업을 수행하는 방법] (그림 참조 : 제프리 리처의 CLR via C# 4판, 28장. I/O 중심의 비동기 작업)
만일 ④ 번 반납된 사용자 스레드가 자발적으로 블로킹을 수행하면 CLR은 CPU 사용 수준을 확인하여, 만일 CPU가 완전히 사용 중이라면 작업을 반납된 스레드에게 할당하지 않습니다. 이렇게 하면 컨텍스트 전환이 감소되어 성능이 개선 됩니다.
내부적으로 이렇게 비동기 I/O 처리를 하는 스레드 풀이 별도로 관리되고 앞서 살펴본 내용들을 구현하기 위해서 I/O 컴플리션 포트(IO Completion Port)라고 부르는 윈도우 리소스를 사용합니다. CLR은 하드웨어 디바이스에 대하여 열기 작업을 수행할 때 I/O 컴플리션 포트를 생성한 후, 디바이스와 I/O 컴플리션 포트를 결합하여, 추후 이 디바이스 드라이버가 작업을 완료한 후, 완료된 IRP를 어디로 큐잉해야 하는지를 판단할 수 있도록 해줍니다.
|