Graphics Programming

윈도우즈 쓰레드 풀 본문

Season 1/Misc

윈도우즈 쓰레드 풀

minseoklee 2017. 10. 5. 10:49

[MSDN] Thread Pools: https://msdn.microsoft.com/en-us/library/windows/desktop/ms686760(v=vs.85).aspx


쓰레드를 직접 생성하고 관리하는 것이 아니라 워크 오브젝트를 쓰레드 풀에 제출한다. 쓰레드를 생성할 때 콜백 함수와 콜백 함수가 받을 인자를 지정하는데, 워크 오브젝트도 마찬가지다. 쓰레드 풀은 윈도우즈 커널이 알아서 관리하며, 제출된 워크 오브젝트들을 적절히 스케줄링하여 처리한다.


// <Windows 시스템 프로그래밍 제4판>의 예제 코드를 조금 수정한 것


#include <Windows.h>

#include <tchar.h>

#include <vector>

#include <set>


/*


1. Invoke InitializeThreadpoolEnvironment().

2. Create work objects.

3. Invoke SubmitTheadpoolWork() to submit work objects.

4. Invoke WaitForThreadpoolWorkCallbacks().

- The thread who called this is blocked until all invocations of work objects are complete.

4. Invoke CloseThreadpoolWork().


*/


const size_t CACHE_LINE_SIZE = 64;

const INT NUM_THREADS = 100000; // Actually the number of work objects, not threads


__declspec(align(CACHE_LINE_SIZE))

struct ThreadArg {

int threadID;

SRWLOCK lock;

};


VOID CALLBACK worker(PTP_CALLBACK_INSTANCE, PVOID, PTP_WORK);


std::set<DWORD> threadIDs;


int _tmain(DWORD argc, LPTSTR argv[]) {

INT iThread;

SRWLOCK lock;

std::vector<PTP_WORK> workObjects;

std::vector<ThreadArg*> threadArgAry;

TP_CALLBACK_ENVIRON cbe; // callback environment


// Initialize resources

workObjects.assign(NUM_THREADS, nullptr);

threadArgAry.assign(NUM_THREADS, nullptr);

InitializeSRWLock(&lock);

InitializeThreadpoolEnvironment(&cbe);

// Create work objects and submit them

for (iThread = 0; iThread < NUM_THREADS; ++iThread) {

threadArgAry[iThread] = (ThreadArg*)_aligned_malloc(sizeof(ThreadArg), CACHE_LINE_SIZE);

ThreadArg& arg = *threadArgAry[iThread];

arg.threadID = iThread;

arg.lock = lock;


workObjects[iThread] = CreateThreadpoolWork(worker, &arg, &cbe);

SubmitThreadpoolWork(workObjects[iThread]);

}


// Wait for threads to complete

for (iThread = 0; iThread < NUM_THREADS; ++iThread) {

WaitForThreadpoolWorkCallbacks(workObjects[iThread], FALSE);

CloseThreadpoolWork(workObjects[iThread]);

}


// Check thread IDs

int totalThreads = 0;

for (auto& id : threadIDs){

printf("Thread %u\n", id);

totalThreads += 1;

}

printf("The number of threads in the pool: %d\n", totalThreads);

//printf("Number of threads: %llu\n", threadIDs.size()); // Not same as totalThreads (???)


system("pause");

return 0;

}


VOID CALLBACK worker(PTP_CALLBACK_INSTANCE instsance, PVOID context, PTP_WORK work) {

ThreadArg* arg = reinterpret_cast<ThreadArg*>(context);

Sleep(1);


DWORD id = GetCurrentThreadId();


AcquireSRWLockExclusive(&arg->lock);

threadIDs.insert(id);

ReleaseSRWLockExclusive(&arg->lock);

}


threadIDs의 크기로 쓰레드 풀에서 쓰레드를 몇 개 운영했는지 확인할 수 있다. 워크 오브젝트의 개수를 바꿔가며 쓰레드 개수를 확인해봤다.


 워크 오브젝트

 쓰레드

 10

 4

 100

 4

 1000

 8

 10000

 20


쓰레드 개수도 대충 선형 증가하지만 worker 함수의 실행 시간, 그리고 단순히 순간 순간의 실행 환경에 따라 달라질 수 있다. 워크 오브젝트 10000개일 때 쓰레드를 2개만 쓴 경우도 있었다. (내부 구현은 잘 모름 -_-) 이상하게 threadIDs.size()가 totalThreads보다 큰 경우가 자주 있는데 원인은 모르겠다. 동기화를 잘못 해서 set 내부가 꼬였나;;


하스켈의 스파크(spark)가 바로 쓰레드 풀의 워크 오브젝트에 대응하는 개념인 것 같다. 얼랭은 아예 모르지만 얼랭에서 경량 쓰레드를 동시에 수십만 개 돌릴 수 있다는 게 쓰레드 풀을 이야기하는 건가 보다. OS 쓰레드를 그렇게 돌릴 수는 없을 것 같다.

Comments