언어/C#

CLR 스레딩1 기본

tsyang 2022. 4. 10. 03:36

윈도우 스레드 도입


응용 프로그램의 인스턴스는 '프로세스' 라고 부르는 공간 내에서 수행된다. 개별 프로세스는 자신만의 가상 주소 공간을 가지고 있어서 다른 프로세스가 자신의 코드나 데이터에 접근할 수 없다. 

 

그러나 만약 한 응용프로그램이 무한 루프에 빠졌다면? 그리고 CPU가 하나밖에 없다면? 이렇게 되면 다른 코드나 사용자의 요청에 응답할 수 없는 상태가 되어버린다. 

 

이러한 문제를 해결하기 위해 만들어 진 것이 '스레드'이다. 스레드는 CPU를 가상화 하기 위한 윈도우 운영체제의 개념이다. 

 

스레드는 비용이 있지만 운영체제의 응답성이 좋아지게 하는 효과가 있다.


 

스레드의 비용


모든 스레드는 다음의 비용을 하나씩은 가진다.

 

  1. 스레드 커널 객체 : OS는 개별 스레드별로 고유의 데이터 구조체를 할당화하고 초기화한다. 이 안에 흔히 말하는 '컨텍스트'라는 정보도 포함되어 있다. 컨텍스트는 CPU 내의 레지스터들의 값을 저장하고 있는 메모리 블록이다.(64비트 기준 1240바이트)
  2. 스레드 환경 블록(TEB) : 스레드의 컨텍스트 정보. 유저 모드(모든 응용프로그램이 빠르게 접근할 수 있는 주소공간)에 할당되고 초기화된다. TIB(Thread information block)라고도 하는 듯.
  3. 유저 모드 스택 : 지역 변수와 함수의 매개변수를 저장. 함수가 반환될 때 다음으로 수행해야 할 위치 기억. (대략 1MB)

  4. 커널 모드 스택 : 커널 모드 함수로 매개변수를 전달해야 할 때 사용 (대략 12~24KB)

  5.  DLL의 스레드 attach/detach 통지 : 스레드가 생성/종료 될 때 모든 비관리 DLL들에게 스래드 생성/종료를 알린다. 일부 프로세스의 경우 수백개의 DLL을 사용하기도 하기 때문에 비용이 제법 클 수 있다.

 

여기에 더해 자신의 퀀텀만큼 일을 한 스레드가 교체 될 때 컨텍스트 전환이라는 비용이 발생한다. 과정은 다음과 같다.


1. 현재 수행 중인 스레드의 CPU 레지스터를 스레드의 컨텍스트 구조체에 저장하고,
2. 가상 메모리 주소를 전환하고,
3. 새로 작업을 수행할 스레드의 컨텍스트 구조체 값들을 CPU 레지스터로 로드함.

 

이런 컨텍스트 전환은 엄청난 비용을 초래할 수 있는데, 만약 CPU가 컨텍스트 전환 없이 계속 이전 스레드를 수행한다면 CPU 캐시를 많이 활용할 수 있지만 컨텍스트가 전환되면 CPU 캐시의 내용이 쓸모 없어지기 때문이다.

 

더 나아가 가비지 수집을 진행할 때 CLR은 모든 스레드를 일시 정지 시킨다. 

 

이처럼 스레드는 가능한 만들지 않는 것이 좋다... 그럼에도 응답성이 좋은 운영체제를 만들기 위해 스레드를 선택할 수 밖에 없었던 것이다.

 

만약 CPU가 여러개라면? 각각의 CPU 코어가 개별적으로 컨텍스트 전환을 수행하기 때문에 스레드를 더 효율적으로 쓸 수 있다. 물론 스레드의 개수가 컴퓨터의 CPU수와 일치한다면 최적이다.

 

인텔의 하이퍼스레드 : 하이퍼 스레드의 경우 CPU 레지스터 같은 구조적인 상태를 저장하기 위한 공간을 두 세트 가지고 있는 것임.

 

 


 

 

스레드 우선순위


윈도우에서는 프로세스가 아니라 스레드를 스케줄의 대상으로 삼는다. 

 

스레드의 우선순위는 프로세스의 우선순위 + 상대 스레드 우선순위를 조합하여 정해줄 수 있다. 우선순위 레벨은 0~31까지 존재한다.

 

0레벨은 제로 페이지 스레드만 존재한다. 이 스레드는 다른 스레드가 없을 때 RAM의 내용을 0으로 바꾸는 역할을 한다.

 

17~30 은 커널 모드에서만 사용 가능하다.

 

프로세스의 우선순위는 [유휴, 보통이하, 보통, 보통이상, 높음, 실시간] 으로 구분된다. 유휴는 화면 보호기같은 프로그램을 작성하기 좋고 높음은 반드시 필요한 경우에만 사용해야 한다. 실시간의 경우엔 너무 높은 우선순위를 가지고 있어서 디스크 I/O 나 네트워크 작업을 방해할 수 있다. 따라서 실시간 프로세스 우선순위는 아주 기민하게 반응해야 하거나, 아주 짧은 시간만 수행되는 작업에만 적용해야 한다. 

 

상대 스레드 우선순위는 [유휴, 가장낮음, 보통이하, 보통, 보통이상, 가장높음, 타임크리티컬] 로 나뉜다. CLR의 finalizer 스레드는 타임 크리티컬 우선순위를 갖는다.

 

다른 스레드의 우선순위를 높이는 것 보다 자신의 우선순위를 낮추는 방법이 더 좋다. 

 

CPU를 100%활용하려면 이런 스레드 우선순위를 신중하게 고려해야 한다.


 

 

 

포그라운드/백그라운드 스레드


CLR은 모든 프로세스를 포그라운드백그라운드 스레드로 간주한다. 프로세스의 모든 포그라운드 스레드가 종료되면 CLR은 백그라운드 스레드들도 같이 종료시키며 이때 예외는 발생되지 않는다.

 

따라서 메모리의 내용을 디스크에 쓰는 것과 같이 반드시 완료해야 하는 작업은 포그라운드 스레드를 이용해야 한다. 그러나 포그라운드 스레드는 가급적 적게 사용하는 것이 좋은데 포그라운드 스레드가 살아있으면 프로세스가 종료되지 않을 수 있기 때문이다.

 

포그라운드 <-> 백그라운드 스레드 전환은 언제든 가능하다.

 

스레드 풀이란 개념이 있는데, 여기의 스레드는 백그라운드 스레드로 작동한다. 

 

 


참고 : 제프리 리처, CLR via C# 4판 , 비제이퍼블릭, 26장. 스레드의 기본 [777~801p]

'언어 > C#' 카테고리의 다른 글

CLR 스레딩3 - 태스크(Task)  (0) 2022.04.24
CLR 스레딩2 단순 계산 작업  (1) 2022.04.17
런타임 Serialization - 1  (1) 2022.04.03
C# - 리플렉션 (Reflection)  (0) 2021.07.04
C# - CLR 호스팅과 앱도메인  (1) 2021.06.27