Graphics Programming
윈도우즈 쓰레드 풀 본문
[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 쓰레드를 그렇게 돌릴 수는 없을 것 같다.