본문 바로가기
C/[서적] 뇌를 자극하는 윈도우즈 시프

Chapter 15. 쓰레드 풀링

by GameStudy 2022. 2. 13.

15.1 쓰레드 풀에 대한 이해

Prologue) 책의 Part4는 윤성우 저자가 실무적인 부분을 집중적으로 집필한 부분.

  그리고 집필 당시, 가장 먼저 집필한 파트. 내용도 프로그램의 성능과 성격을

  결정짓는 Memory Management, I/O에 대한 내용이 들어있어서 꽤나 중요한 파트.

 

15.1-1 쓰레드 풀이란

  Note) 쓰레드 풀은 대표적인 FrameWork.

    - 쓰레드 풀은 제공되는 API를 이용해서 작성하면 쉽게 구현할 수 있음.

      그래서 막상 구현해보라 하면 뭔진 아는데 구현을 못함. 

    - FrameWork로 제공이 되지만, 직접 구현해 봄으로써 쓰레드 관련 내용인 

      동기화 기법들이 하나의 모델로 활용이 됨. 그래서 종합적인 점검의 느낌.

 

  Note) 쓰레드 풀이 왜 필요할까?

    - 쓰레드라 생각하지말고, 커널 오브젝트를 동반하는 리소스라 해보자.

      이런 리소스를 생성하고 소멸하는데엔 큰 Overhead가 발생함.

      커널모드와 유저모드사이의 전환, 새로운 리소스를 OS에 대한 정보 등록, 메모리 동적할당, etx...

    - 근데 위는 커널 오브젝트를 동반하는 리소스의 경우임. 쓰레드는 더함.

      왜냐하면 스케줄러에 의해 스케줄링되어야 하기 때문에 또하나의 부담이 추가됨.

    - 만약 10개의 작업이 하나씩 하나씩 들어온다면?

      Overhead를 줄이기 위해서 쓰레드 하나만 생성해서 작업을 처리하고 소멸하면됨.

      그럼 쓰레드 9개 만큼의 Overhead를 줄일 수 있음.

      근데 처음엔 2개가, 그다음엔 3개 그다음엔 5개가 동시에 밀어닥친다면?

      5개의 쓰레드만 생성해서 작업을 처리하고 소멸하면 됨.

      그럼 5개 만큼의 Overhead를 줄일 수 있음.

      이렇게 생성된 5개의 쓰레드를 어딘가에 저장해 두고자 함. 즉 쓰레드 풀.

    - 쓰레드 풀에 5개의 쓰레드를 미리 생성 해 둠.

      2개의 작업이 동시에 들어오면 쓰레드 2개를 깨워서 이 작업들을 수행.

    - 선행 2개의 작업이 아직 끝나지 않았는데 3개가 동시에 들이닥치면 

      3개의 쓰레드를 깨워서 이 작업들을 수행. 이와중에 선행 2개 작업이 끝나면,

      작업을 마친 2개의 쓰레드를 다시 쓰레드 풀에 집어넣음.

    - 직전 3개의 작업마저 끝나면 3개의 쓰레드를 다시 쓰레드 풀에 집어넣음

      그리고 동시에 들어온 5개의 작업을 5개의 쓰레드를 꺼내서 동시에 진행함.

    이런 부분들이 Overhead를 최적화하면서도, 멀티 쓰레드를 통한 성능향상의 길임.

    그런데 막상 구현해보고자 하면, 어떻게 작업을 할당할 것이며

    어떻게 메모리 풀에 반환할 것인가가 가장 고민됨. 왜냐면 우린 CreateThread() 밖에 모름.

    이제 실질적인 모델을 배워서 이에 대한걸 학습해 보자.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

 

15.2 쓰레드 풀의 구현

 

15.2-1 쓰레드 풀의 구현 원리

  Note) 두 가지 궁금증.

    1. WORK는 뭐고, WORK를 어떻게 쓰레드에게 할당하지?

    2. 쓰레드 관리. WORK이 있으면 깨우고, 없으면 다시 쓰레드 풀에 저장하는 방법은?

    아래의 모델은 아직 완전하진 않지만, 위 내용을 배우기엔 적합한 모델.

// 작업이라는 건, 함수로 구현 가능함. 즉, 작업의 기본 단위는 함수.
// 반환 자료형과 매개변수 목록은 프로그래머 마음대로 가능.
typedef void(*WORK)(void);

// 사실 쓰레드 풀은 Framework라 했음. 즉, OS에서 제공되는 API들.
// 구현 하려면 마치 윈도우즈가 쓰레드를 관리하듯이, 우리도 비슷하게 구현 해보자.
// 이를 위해서 아래와 같은 구조체가 필요함. 단순하게 두 멤버만 추가함.
typedef struct __WorkerThread
{
    HANDLE hThread;
    DWORD idThread;
    // 이외에도 우선순위와 같은 멤버도 추가 가능. 일의 성격, etc ...
} WorkerThread;

// Work와 Thread 관리를 위한 구조체. Thread Pool의 기본적인 모델. 꼭 이렇게 구현하라는 표준이 아님에 유의.
typedef struct __ThreadPool
{
    // Work을 등록하기 위한 배열.
    WORK workList[WORK_MAX];
    
    // Thread 정보와 각 Thread별 Event Object
    WorkerThread workThreadList[THREAD_MAX]; // Thread 정보를 저장할 수 있는 배열
    HANDLE workerEventList[THREAD_MAX];      // Event Object 정보를 저장할 수 있는 배열
    
    // Work에 대한 정보
    DWORD idxOfCurrentWork;                  // 대기 1순위 Work Index.
    DWORD idxOfLastAddedWork;                // 마지막 추가 Work Index + 1.
    
    // Number of Thread
    DWORD idxOfThread;                       // Pool에 존재하는 Thread의 갯수.

} gThreadPool;

 

  Note) WorkList 배열에 대한 설명

    - 기본적으로 함수 포인터가 저장되는 배열. 

      저장된 함수 포인터를 쓰레드에다가

      하나씩 하나씩 순차적으로 할당해 줄 수 있게 하는 배열.

    - 배열 대신에 Linked list 혹은 Ring queue로 구현하는 것이 좀 더 좋음.

      자료구조 시간이 아니기에 여기선 그냥 배열로 함.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 쓰레드 풀에 대한 설명

    1. 쓰레드 등록: MakeThreadToPool()

      쓰레드 풀에 쓰레드를 생성. 동시에 Event Object도 생성함.

      왜 Event Object도 필요할까? 쓰레드가 쓰레드 풀에 있다는건,

      아직 할당된 작업이 없다는 뜻. 따라서 이때는 스케줄링되면 안됨. 

      할일이 없는데 실행 시간을 할당받아서는 안됨. 

      그래서 작업이 없는 쓰레드는 잠재우기 위해서 Event Object가 필요함.

      쓰레드는 WaitForSingleObject() 함수를 통해 Event Object를 감시함.

      Event Object는 생성과 동시에 Non-signaled 상태에 있기 때문에,

      쓰레드는 Blocked 상태가 됨. 

    2. WORK 등록: AddWorkToPool()

    3. Event Object를 Signaled 상태로 전환

    4. WaitForSingleObject() 함수의 반환. Thread는 Running 상태로 전환.

    5. WORK 할당: GetWorkFromPool()

      4번에서 벌떡 일어난 쓰레드가 GetWorkFromPool()을 통해서 

      함수 포인터를 하나 얻어서 호출 하면 됨.

    6. Thread 반환

      쓰레드 풀에 담겨 있는 쓰레드는 WaitForSingleObject()와 할당 받은 함수 포인터

      둘을 반복적으로 실행하는 식으로 구현함.

      그래서 쓰레드의 Event Object를 Non-signaled 상태로 전환 해 두기만 하면
      알아서 쓰레드는 Blocked 상태로 갈 예정. 이게 쓰레드 풀로의 쓰레드 반환.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) Intelligent Pool

    쓰레드의 갯수를 각 순간에 몇개 생성하고 소멸할지를 결정 가능한 풀.

    처음에 작업 없을때 미리 몇개 생성할지, 중간에 일이 밀려 들어올때 몇 개 더 생성할지

    일이 없어서 한 두개의 쓰레드에 실행 시간을 몰아주기 위해서 몇 개 소멸 시킬지 가능.

    근데 이는 굉장히 실무적인 이야기. 작업이 무슨 작업이냐에 따라 많이 달라짐.

    다양한 정보와 테스트 결과들이 필요함. 한 사람이 만들기 보단, 여러 학문의 사람들이 뭉침.

댓글