ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스레드 풀 스레드 관리
    @ 17. 1 ~ 18/C# 멀티스레드 2018. 6. 30. 16:15

    CLR 스레드 풀이 스레드 풀에서 스레드를 언제 생성하고 파괴하는지 어떻게 결정하고

    어떻게 개발자가 이런 프로세스에 영향을 미칠 수 있는지 보자


    * 스레드 인젝션과 은퇴 알고리즘 세부사항

    윈도우 스레드 풀과 동일하게 CLR 풀은 일부 정교한 휴리스틱을 사용해 스레드의 관리를 추상화한다.

    그러나 채용된 구체적인 휴리스틱은 다르다. 

    대부분의 사람들은 구체적인 알고리즘을 몰라도 된다.

    하지만 내부 세부구현 내용을 이해하는것이 프로그램의 성능과 확장성을 이해하는데 도움이 된다.

    실질적으로 CLR 스레드 풀은 두가지 세트의 스레드를 관리한다

    하나는 일반적인 작업 아이템을 처리하고(QueueUserWorkItem, 콜백 등..) 다른하나는 모든 I/O완성을 다룬다.

    특성이 달라도 두 가지의 스레드 관리는 거의 동일하다.  주요 차이점은 스레드의 큐에 작업이 어떻게 입력되는지에 있다.

    작업 스레드의 경우 사용자 풀과 관련된 작업 큐가 있지만, I/O 스레드의 경우 모든 과정이 I/O완성 포트를 통해 발생한다.

    게다가 I/O완성 포트는 실행되는 스레드의 수를 제한한다.


    작업이 풀의 큐에 입력될때 스레드 풀은 최적의 스레드 수에 도달할때까지 호출한 스레드에 스레드를 생성할 것이다.

    이런 최적의 수는 현재 기계의 프로세서의 수다. 이수에 도달하면 CLR은 스레드의 생성을 제한한다.


    * 최소와 최대스레드

    최소와 최대작업 스레드와 최소와 최대 I/O스레드가 있다. 두가지 모두 최소값은 0스레드이다. 이는 프로세스는 풀에 전용 스레드가 하나도 없이

    시작하고 작업이 없는 시간동안 풀은 하나도 없는 상태로 되돌아 올 수 있다는 것을 의미한다.

    기본 최대값은 프로세서당 250개이다. I/O스레드는 이값은 항상 1000이다.

    ASP.NET 2.0의 경우 자동설정으로 최소값은 프로세서당 50 최대값은 100으로 설정되어있다.


    이런값을 ThreadPool 클래스는 정적 메소드인 SetMaxThreads 와 SetMinThread를 통해서 값을 변경할 수 있다.

    public static void SetMaxThreads {

    int workerThreads;

    int completionPortThreads

    }


    풀의 기본 값은 프로세서당 250개 이므로 4개의 프로세서 기계를 갖고 최대 작업 스레드 수를 묻는다면 1000을 반환한다.

    많은 프로그램에서 기본 값은 충분하다.

    성능 테스트와 분석 동안 작업 부하에 구체적인 차단 비율에 기반을 두고 다른 값으로 실험하는 것은 흔한 일이다.

    이론적으로 프로세서당 하나의 스레드를 갖는 것은 가능한 가장 최상의 성능을 낸다.

    (적은 컨텍스트 스위치와 캐시 스레싱으로 인해)

    그러나 이렇게 되면 실질적으로는 스레드는 규칙적으로 차단된다. 

    스레드가 차단될때 스레드 풀은 다른 작업을 처리할 다른 스레드가 필요하고 그렇지 않으면 프로세서 사용률을 야기할 수 있다.

    스레드가 차단되고 큐에 작업이 있다면 스레드 풀이 재빠르게 큐에 있는 다른 스레드를 던짐으로써 반응하길 원할 것 이다.

    반면 너무 많은 스레드를 가지면 높은 컨텍스트 스위치 오버헤드와 많은 캐시오류를 유발할 수 있다.

    스레드가 항상 계산 위주라면 프로세서의 수보다 많은 스레드는 낭비다.

    새로운 스레드를 즉시 생성하는 것은 지나친것일지도 모른다. 

    스레드풀은 스레드를 만들 때 많은  요소를 재고 이런 동작에 영향을 미칠 수 있는 유일한 방법은 최소와 최대값을 변경하는 것이다.

    기본 값을 변경하는 이유는 성능에 대한 동기 말고도 프로세서당 250 작업 스레드의 기본 값들중 대부분은 그냥 방치된다.



    낮은 최대값으로 인한 데드락

    최대 수의 스레드를 모두 써버리는 것이다. 이때 스레드 풀은 현재 값이 최대치에 도달하면 새로운 스레드의 생성을 멈춘다.

    이떄 최대값이 너무 낮으면 프로그램은 데드락에 빠질 가능성이 있다.

    1. 스레드 t0는 작업 아이템 w0를 스레드 풀의 큐에 넣는다.

    2. w0는 32개의 새로운 작업 아이템 w1....w32를 스레드 풀의 큐에 넣는다.

    3. w0는 스레드 풀의 스레드를 차단함으로써 w1....w32가 완료되길 기다린다.


    w1....w32가 스레드 풀의 스레드에 할당되고 최대 스레드 수를 가졌을떄 무엇을 하는지에 따라 이 프로그램은 데드락에 빠질지도 모른다.

    왜냐면 최대값이 25개라고 할때 모든 32개의 작업이 동시에 실행될 수 없다. 처음 24개가 실행되고 몇 개가 끝나면 나머지가 실행되어야 하지만

    만약에 32번쨰의 작업 아이템이 모든 다른 스레드가 완성되기 전에 읽어야하는 플래그가 있다면?? 절대 끝나지 않는다.


    위에 문제점은 작업 아이템(큐잉)간에 되도록 상호의존성을 최대한 피해야하며, 스레드 풀의 스레드가 차단되는 것을 피하기 위해 노력해야한다.

    결국 왜 250개의 스레드가 기본이냐면 CLR팀에서 모든 프로그램이 이렇게 많은 스레드의 사용을 기대하지는 않지만

    차라리 데드락을 피하는걸 선택한것이다.



    낮은 최소값으로 인한 데드락

    스레드 수가 기계의 프로세서 수를 넘어서면 스레드 풀은 새로운 스레드를 생성하는 데 하나당 500밀리초의 비율로 조절한다.

    다음의 경우를 보자

    1. 4개의 프로세서를 가진 웹서버가 리부트됐고 프로세스가 방금 생성됐다.

    2. 16개의 새로운 웹 요청이 거의 동시에 도착한다.

    3. CLR 스레드 풀은 재빨리 처음 4개의 스레드를 만듦으로써 반응한다.

    이때 스레드의 수가 프로세서의 수보다 작게 유지될 때는 제한을 하지 않기 때문에 새로운 작업은 지연 없이 큐에 넣어진다.

    4. 어떤 이유로든 활발히 요청을 수행하는 각 4개는 차단된다.

    5. 500밀리초 후에 CLR 스레드 풀은 요청이 차단된것을 알아차리고 다섯 번째 요청을 서비스하기 위해 하나의 스레드를 생성함으로써 반응한다.

    스레드 풀은 4개가 아닌 단지 1개의 스레드를 생성한다.

    6. 다시 500밀리초가 지나도 다른 5개의 스레드는 여전히 차단돼 있다고 가정하면

    스레드 풀은 추가 작업을 서비스하기 위해 다른 스레드를 생성한다.

    7. 계속 진행..


    차단되는 시간에 따라 이 동작은 매우 나쁠 수도 있다. 500밀리초보다 길게 차단하는 것이 일생에 해당하지만 이런 경우도 발생할 수 있다.

    여기서는 극단적인 예를 제공한것이다.


    처음 15개의 요청이 긴 시간동안 차단됐다고 하면 16번재 요청이 서비스되려면 6초를 기다려야 한다.

    (500ms 시간이 걸리는고 16개중 14개를 제외한 12개의 시간을 측정한 것)

    이 예제에서 서버가 끊임없는 부하를 갖고 작업 부하가 일정하다면 결국 풀은 최적의 스레드 수로 준비될 것이다.

    (나중에 생성될것들이 다 생성된 후를 이야기한다.)

    그리고 지연이 줄어드는 것이 나타난다.


    결국 기본 최솟값을 스스로 변경할 것을 고려해야하는 확실한 이유가 이런이유 때문이다.

    (최소값이 높다면? 교체에 따른 컨텍스트 비용과 낮은 cpu사용률 이 문제인건 이제 다들아는 사실)

    마법의 숫자는 없다. 측정하고 개선하고 이런수밖에..


    '@ 17. 1 ~ 18 > C# 멀티스레드' 카테고리의 다른 글

    메모리 락과 락없는 프로그래밍  (0) 2018.07.03
    읽기 / 쓰기 락  (0) 2018.06.27
    스레드 로컬 저장소  (0) 2018.06.21
    스레드 동기화란?  (0) 2018.03.05
    동시성 컬렉션 사용  (0) 2017.08.20
Designed by Tistory.