Windows 스레드 풀
이 글은 윤성우 님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.
스레드를 사용한다면 자연스럽게 스레드 풀에 대해 알게 됩니다. 왜냐하면 스레드를 생성/해제할 때마다 서버가 지연되고 계속해서 한도 없이 새로운 스레드로 생성하여 요청을 처리하면 누적되는 부하에 서버가 중단되는 상황이 많이 발생하기 때문입니다. 서버가 꼭 멀티 스레드를 사용하는 것은 아니지만 I/O burst가 많다면 멀티 스레드를 사용하는 것이 유리합니다. 물론 입출력 다중화를 사용하는 방법도 유효합니다.
Thread Pool
스레드 풀은 위에서 말한 문제점을 해결하면서 여러 작업을 효율적으로 처리하기 위해 스레드를 집합으로 관리하는 것을 말합니다. 스레드 풀에 생성된 스레드들은 계속해서 생성 및 해제되지 않고 작업이 할당되기 전까지 대기합니다. 그리고 작업이 할당된 스레드는 대기를 해제하고 작업을 수행하는 구조입니다.
사용자가 직접 스레드 풀을 구현하는 방법도 있지만 Win32 API를 사용하여 구현할 수도 있습니다. Win32 API를 통해 구현하게 되면 성능에 따른 풀 크기와 작업 스케줄링을 자동적으로 관리해 줍니다. 또한 내부가 이미 최적화되어 구현되어 있기 때문에 시스템의 안정성과 성능을 보장할 수 있습니다. 유지보수에서도 차이가 있습니다. 사용자가 직접 구현하게 된다면 동기화 객체, 큐 구조체 등 내부 구조를 이해해야만 유지보수를 할 수 있지만 Win32 API는 공식 문서만 이해한다면 상대적으로 직접 구현된 스레드 풀보다 유지보수가 용이합니다.
Thread Pool Functions
우선 대표적인 스레드 풀 함수들에 대해 알아보겠습니다
PTP_POOL CreateThreadpool(
PVOID reserved
);
이 함수는 새로운 스레드 풀을 할당합니다. 사용자가 정의한 스레드 풀을 생성할 때 사용합니다. 매개변수는 예약되어 있기 때문에 무조건 NULL을 넣어야 합니다. 반환값은 할당한 스레드 풀의 구조체 포인터입니다.
BOOL SetThreadpoolThreadMinimum(
[in, out] PTP_POOL ptpp,
[in] DWORD cthrdMic
);
void SetThreadpoolThreadMaximum(
[in, out] PTP_POOL ptpp,
[in] DWORD cthrdMost
);
위 두 함수는 할당한 스레드 풀 스레드 수의 최솟값/최댓값을 설정합니다.
- ptpp : CreateThreadpool 함수를 통해 할당받은 스레드 풀 구조체 포인터
- cthrdMic/cthrdMost: 스레드 풀에 설정할 최솟값/최댓값
SetThreadpoolThreadMinimum 함수는 반환값으로 성공, TRUE/실패, FALSE 여부를 반환합니다.
void InitializeThreadpoolEnvironment(
[out] PTP_CALLBACK_ENVIRON pcbe
);
이 함수는 스레드 풀 환경을 초기화합니다. 위에서 할당받은 사용자 정의 스레드 풀을 사용하려면 우선 이 함수에서 전달받은 스레드 풀 환경을 통해 스레드 풀을 설정해야 합니다. 매개변수를 통해 환경을 전달하기 때문에 매개변수는 전달받을 TP_CALLBACK_ENVIRON 구조체의 주소를 넣어야 합니다.
void SetThreadpoolCallbackPool(
[in, out] PTP_CALLBACK_ENVIRON pcbe,
[in] PTP_POOL ptpp
);
이 함수를 통해 사용자 정의 스레드 풀을 환경에 설정합니다.
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체의 포인터
- ptpp: CreateThreadpool로 할당한 뒤 수정한 사용자 정의 스레드풀의 구조체 포인터
실행이 끝나면 첫 번째 매개변수로 넣었던 환경을 이용하여 사용자 정의 스레드 풀을 사용할 수 있습니다.
PTP_WORK CreateThreadpoolWork(
[in] PTP_WORK_CALLBACK pfnwk,
[in, out, optional] PVOID pv,
[in, optional] PTP_CALLBACK_ENVIRON pcbe
);
이 함수는 스레드 풀에 할당할 작업 객체를 생성합니다. 이름 때문에 헷갈릴 수 있는데 스레드 풀을 생성하는 것이 아닌 스레드 풀에 사용할 객체를 생성하는 함수입니다.
- pfnwk: 스레드가 작업을 할당받은 후 호출할 콜백 함수
- pv: 콜백 함수에 전달할 데이터
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체 포인터 혹은 NULL, 시스템 스레드 풀
반환값은 생성된 작업 객체의 포인터입니다. NULL을 반환한 경우 실패입니다.
PTP_WAIT CreateThreadpoolWait(
[in] PTP_WAIT_CALLBACK pfnwa,
[in, out, optional] PVOID pv,
[in, optional] PTP_CALLBACK_ENVIRON pcbe
);
이 함수도 위 CreateThreadpoolWork 함수와 비슷하게 스레드 풀에 할당할 대기 객체를 생성합니다. 대기 객체는 다른 객체를 참조하여 객체가 Signaled 상태가 되거나 혹은 제한 시간이 만료되면 콜백 함수를 호출합니다. 주로 이벤트 객체와 함께 사용됩니다.
- pfnwa: 대기 후 완료 시 호출할 콜백 함수
- pv: 콜백 함수에 전달할 데이터
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체 포인터 혹은 NULL, 시스템 스레드 풀
반환값은 역시 생성된 대기 객체 포인터입니다. NULL을 반환한 경우 실패입니다.
PTP_TIMER CreateThreadpoolTimer(
[in] PTP_TIMER_CALLBACK pfnti,
[in, out, optional] PVOID pv,
[in, optional] PTP_CALLBACK_ENVIRON pcbe
);
이 함수도 스레드 풀에 할당할 타이머 객체를 생성합니다. 타이머 객체는 동기화의 타이머 객체와 비슷합니다. 주기적으로 타이머 객체가 만료될 때마다 콜백 함수를 호출할 수 있으며 혹은 단발성으로 콜백 함수를 호출할 수 있습니다.
- pfnwa: 시간이 만료될 때마다 호출할 콜백 함수
- pv: 콜백 함수에 전달할 데이터
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체 포인터 혹은 NULL, 시스템 스레드 풀
동일하게 반환값은 생성된 타이머 객체 포인터입니다. NULL을 반환한 경우 실패입니다.
PTP_IO CreateThreadpoolIo(
[in] HANDLE fl,
[in] PTP_WIN32_IO_CALLBACK pfnio,
[in, out, optional] PVOID pv,
[in, optional] PTP_CALLBACK_ENVIRON pcbe
);
이 함수는 스레드 풀에 할당할 I/O completion 객체를 생성합니다. I/O completion은 I/O작업이 완료되었을 때 알림을 받을 수 있습니다. 이를 이용하여 해당 I/O작업이 완료될 때마다 실행할 콜백 함수를 호출합니다.
- fl: I/O completion 객체에 바인딩할 파일 핸들
- pfnwa: 작업이 완료될 때마다 호출할 콜백 함수
- pv: 콜백 함수에 전달할 데이터
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체 포인터 혹은 NULL, 시스템 스레드 풀
반환값은 생성된 I/O completion 객체 포인터입니다. NULL을 반환한 경우 실패입니다.
PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();
이 함수는 하나 이상의 스레드 풀을 추적하여 해제할 그룹을 생성합니다. 반환값은 생성된 그룹의 포인터입니다.
void SetThreadpoolCallbackCleanupGroup(
[in, out] PTP_CALLBACK_ENVIRON pcbe,
[in] PTP_CLEANUP_GROUP ptpcg,
[in, optional] PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);
이 함수는 CreateThreadpoolCleanupGroup을 통해 생성한 그룹에 스레드 풀 환경을 연결합니다.
- pcbe: InitializeThreadpoolEnvironment 함수로 초기화한 환경 구조체 포인터
- ptpcg: CreateThreadpoolCleanupGroup함수로 생성한 그룹 포인터
- pfng: 스레드 풀 환경에 연결된 객체가 해제되기 전에 그룹이 취소된 경우 호출할 콜백 함수
void CloseThreadpoolCleanupGroupMembers(
[in, out] PTP_CLEANUP_GROUP ptpcg,
[in] BOOL fCancelPendingCallbacks,
[in, out, optional] PVOID pvCleanupContext
);
이 함수는 해제 그룹에 연결된 객체들을 해제하고 콜백 함수의 종료를 기다리며 아직 미처리된 콜백 함수를 취소할 수도 있습니다.
- ptpcg: 연결된 그룹 포인터
- fCancelPendingCallbacks: TRUE면 아직 시작하지 않은 미처리 콜백 함수를 취소, FASLE면 취소하지 않고 모두 실행될 때까지 대기
- pvCleanupContext: SetThreadpoolCallbackCleanupGroup에서 등록한 콜백 함수에 전달할 데이터
Using the Thread Pool Functions
나머지 함수들은 Microsoft Learn에 있는 예제를 통해 알아보겠습니다.
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
//
// Thread pool wait callback function template
//
VOID
CALLBACK
MyWaitCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_WAIT Wait,
TP_WAIT_RESULT WaitResult
)
{
// Instance, Parameter, Wait, and WaitResult not used in this example.
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Wait);
UNREFERENCED_PARAMETER(WaitResult);
//
// Do something when the wait is over.
//
_tprintf(_T("MyWaitCallback: wait is over.\n"));
}
//
// Thread pool timer callback function template
//
VOID
CALLBACK
MyTimerCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_TIMER Timer
)
{
// Instance, Parameter, and Timer not used in this example.
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Timer);
//
// Do something when the timer fires.
//
_tprintf(_T("MyTimerCallback: timer has fired.\n"));
}
//
// This is the thread pool work callback function.
//
VOID
CALLBACK
MyWorkCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_WORK Work
)
{
// Instance, Parameter, and Work not used in this example.
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Work);
BOOL bRet = FALSE;
//
// Do something when the work callback is invoked.
//
{
_tprintf(_T("MyWorkCallback: Task performed.\n"));
}
return;
}
VOID
DemoCleanupPersistentWorkTimer()
{
BOOL bRet = FALSE;
PTP_WORK work = NULL;
PTP_TIMER timer = NULL;
PTP_POOL pool = NULL;
PTP_WORK_CALLBACK workcallback = MyWorkCallback;
PTP_TIMER_CALLBACK timercallback = MyTimerCallback;
TP_CALLBACK_ENVIRON CallBackEnviron;
PTP_CLEANUP_GROUP cleanupgroup = NULL;
FILETIME FileDueTime;
ULARGE_INTEGER ulDueTime;
UINT rollback = 0;
InitializeThreadpoolEnvironment(&CallBackEnviron);
//
// Create a custom, dedicated thread pool.
//
pool = CreateThreadpool(NULL);
if (NULL == pool) {
_tprintf(_T("CreateThreadpool failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 1; // pool creation succeeded
//
// The thread pool is made persistent simply by setting
// both the minimum and maximum threads to 1.
//
SetThreadpoolThreadMaximum(pool, 1);
bRet = SetThreadpoolThreadMinimum(pool, 1);
if (FALSE == bRet) {
_tprintf(_T("SetThreadpoolThreadMinimum failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
//
// Create a cleanup group for this thread pool.
//
cleanupgroup = CreateThreadpoolCleanupGroup();
if (NULL == cleanupgroup) {
_tprintf(_T("CreateThreadpoolCleanupGroup failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 2; // Cleanup group creation succeeded
//
// Associate the callback environment with our thread pool.
//
SetThreadpoolCallbackPool(&CallBackEnviron, pool);
//
// Associate the cleanup group with our thread pool.
// Objects created with the same callback environment
// as the cleanup group become members of the cleanup group.
//
SetThreadpoolCallbackCleanupGroup(&CallBackEnviron,
cleanupgroup,
NULL);
//
// Create work with the callback environment.
//
work = CreateThreadpoolWork(workcallback,
NULL,
&CallBackEnviron);
if (NULL == work) {
_tprintf(_T("CreateThreadpoolWork failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 3; // Creation of work succeeded
//
// Submit the work to the pool. Because this was a pre-allocated
// work item (using CreateThreadpoolWork), it is guaranteed to execute.
//
SubmitThreadpoolWork(work);
//
// Create a timer with the same callback environment.
//
timer = CreateThreadpoolTimer(timercallback,
NULL,
&CallBackEnviron);
if (NULL == timer) {
_tprintf(_T("CreateThreadpoolTimer failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 4; // Timer creation succeeded
//
// Set the timer to fire in one second.
//
ulDueTime.QuadPart = (ULONGLONG) -(1 * 10 * 1000 * 1000);
FileDueTime.dwHighDateTime = ulDueTime.HighPart;
FileDueTime.dwLowDateTime = ulDueTime.LowPart;
SetThreadpoolTimer(timer,
&FileDueTime,
0,
0);
//
// Delay for the timer to be fired
//
Sleep(1500);
//
// Wait for all callbacks to finish.
// CloseThreadpoolCleanupGroupMembers also releases objects
// that are members of the cleanup group, so it is not necessary
// to call close functions on individual objects
// after calling CloseThreadpoolCleanupGroupMembers.
//
CloseThreadpoolCleanupGroupMembers(cleanupgroup,
FALSE,
NULL);
//
// Already cleaned up the work item with the
// CloseThreadpoolCleanupGroupMembers, so set rollback to 2.
//
rollback = 2;
goto main_cleanup;
main_cleanup:
//
// Clean up any individual pieces manually
// Notice the fall-through structure of the switch.
// Clean up in reverse order.
//
switch (rollback) {
case 4:
case 3:
// Clean up the cleanup group members.
CloseThreadpoolCleanupGroupMembers(cleanupgroup,
FALSE, NULL);
case 2:
// Clean up the cleanup group.
CloseThreadpoolCleanupGroup(cleanupgroup);
case 1:
// Clean up the pool.
CloseThreadpool(pool);
default:
break;
}
return;
}
VOID
DemoNewRegisterWait()
{
PTP_WAIT Wait = NULL;
PTP_WAIT_CALLBACK waitcallback = MyWaitCallback;
HANDLE hEvent = NULL;
UINT i = 0;
UINT rollback = 0;
//
// Create an auto-reset event.
//
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (NULL == hEvent) {
// Error Handling
return;
}
rollback = 1; // CreateEvent succeeded
Wait = CreateThreadpoolWait(waitcallback,
NULL,
NULL);
if(NULL == Wait) {
_tprintf(_T("CreateThreadpoolWait failed. LastError: %u\n"),
GetLastError());
goto new_wait_cleanup;
}
rollback = 2; // CreateThreadpoolWait succeeded
//
// Need to re-register the event with the wait object
// each time before signaling the event to trigger the wait callback.
//
for (i = 0; i < 5; i ++) {
SetThreadpoolWait(Wait,
hEvent,
NULL);
SetEvent(hEvent);
//
// Delay for the waiter thread to act if necessary.
//
Sleep(500);
//
// Block here until the callback function is done executing.
//
WaitForThreadpoolWaitCallbacks(Wait, FALSE);
}
new_wait_cleanup:
switch (rollback) {
case 2:
// Unregister the wait by setting the event to NULL.
SetThreadpoolWait(Wait, NULL, NULL);
// Close the wait.
CloseThreadpoolWait(Wait);
case 1:
// Close the event.
CloseHandle(hEvent);
default:
break;
}
return;
}
int main( void)
{
DemoNewRegisterWait();
DemoCleanupPersistentWorkTimer();
return 0;
}
이 예제는 주석 때문에 길어 보이지만 간단하게 스레드 풀을 생성하여 대기, 작업, 타이머 객체를 각각 생성하여 스레드 풀에 할당하는 코드입니다. 우선 main부터 보겠습니다.
int main( void)
{
DemoNewRegisterWait();
DemoCleanupPersistentWorkTimer();
return 0;
}
main에서 첫 번째 함수가 대기 객체를, 두 번째 함수가 작업, 타이머를 시연합니다. 가장 먼저 실행할 DemoNewRegisterWait 함수를 확인해 보겠습니다.
DemoNewRegisterWait
VOID
DemoNewRegisterWait()
{
PTP_WAIT Wait = NULL;
PTP_WAIT_CALLBACK waitcallback = MyWaitCallback;
HANDLE hEvent = NULL;
UINT i = 0;
UINT rollback = 0;
가장 먼저 사용할 변수들을 초기화합니다. 이때 대기 객체에 설정할 콜백 함수는 아래와 같습니다.
VOID
CALLBACK
MyWaitCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_WAIT Wait,
TP_WAIT_RESULT WaitResult
)
{
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Wait);
UNREFERENCED_PARAMETER(WaitResult);
_tprintf(_T("MyWaitCallback: wait is over.\n"));
}
UNREFERNCED_PARAMETER 매크로는 사용하지 않는 매개변수의 Warning 혹은 Error를 발생시키지 않게 해 줍니다. 간단한 출력 함수입니다.
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (NULL == hEvent) {
return;
}
rollback = 1;
Wait = CreateThreadpoolWait(waitcallback,
NULL,
NULL);
if(NULL == Wait) {
_tprintf(_T("CreateThreadpoolWait failed. LastError: %u\n"),
GetLastError());
goto new_wait_cleanup;
}
rollback = 2;
이후 대기 객체와 함께 사용될 이벤트 객체도 생성합니다. 이후 대기 객체를 생성합니다. 이때 3번째 매개변수에 NULL을 넣음으로써 시스템 스레드 풀을 사용합니다.
for (i = 0; i < 5; i ++) {
SetThreadpoolWait(Wait,
hEvent,
NULL);
SetEvent(hEvent);
Sleep(500);
WaitForThreadpoolWaitCallbacks(Wait, FALSE);
}
이제 대기 객체를 사용합니다. 이때 사용할 함수는 SetThreadpoolWait으로 첫 번째 매개변수는 사용할 대기 객체이며 두 번째 매개변수는 참조할 객체입니다. 마지막 세 번째는 제한 시간입니다. NULL을 넣을 경우 두 번째 매개변수 객체가 Signaled 상태가 되기 전까지 계속 대기합니다.
SetEvent 함수를 통해 대기 객체가 참조하고 있는 이벤트 객체를 Signaled 상태로 변경합니다. 이후 등록한 간단한 출력 콜백 함수가 실행됩니다.
마지막으로 WaitForThreadpoolWaitCallbacks 함수는 현재 대기 객체의 콜백 함수가 완료될 때까지 대기합니다. 혹은 아직 실행되지 않았다면 콜백 함수를 취소할 수 있습니다. 두 번째 인자가 취소여부를 뜻하며 TRUE면 미처리된 콜백 함수를 취소합니다. FALSE면 반대로 미처리된 콜백 함수가 완료될 때까지 대기합니다.
반복문을 사용했기 때문에 5개의 대기 콜백 함수의 문장이 출력될 것입니다.
new_wait_cleanup:
switch (rollback) {
case 2:
SetThreadpoolWait(Wait, NULL, NULL);
CloseThreadpoolWait(Wait);
case 1:
CloseHandle(hEvent);
default:
break;
}
return;
}
사용한 자원들을 해제합니다. 여기서는 rollback 변수를 통해 단계별 해제를 구현했습니다. 중간에 SetThreadpoolWait을 한번 더 하는 이유는 참조된 객체의 연결을 끊기 위해서입니다. 이후 각각 함수를 통해 자원을 해제합니다.
DemoCleanupPersistentWorkTimer
VOID
DemoCleanupPersistentWorkTimer()
{
BOOL bRet = FALSE;
PTP_WORK work = NULL;
PTP_TIMER timer = NULL;
PTP_POOL pool = NULL;
PTP_WORK_CALLBACK workcallback = MyWorkCallback;
PTP_TIMER_CALLBACK timercallback = MyTimerCallback;
TP_CALLBACK_ENVIRON CallBackEnviron;
PTP_CLEANUP_GROUP cleanupgroup = NULL;
FILETIME FileDueTime;
ULARGE_INTEGER ulDueTime;
UINT rollback = 0;
여기서는 작업, 타이머 객체를 시연하기 위해서 더 많은 변수를 초기화합니다. 그리고 설정할 콜백 함수들은 아래와 같습니다.
VOID
CALLBACK
MyTimerCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_TIMER Timer
)
{
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Timer);
_tprintf(_T("MyTimerCallback: timer has fired.\n"));
}
타이머 객체에 설정될 콜백 함수입니다. 동일하게 간단한 출력 함수입니다.
VOID
CALLBACK
MyWorkCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Parameter,
PTP_WORK Work
)
{
UNREFERENCED_PARAMETER(Instance);
UNREFERENCED_PARAMETER(Parameter);
UNREFERENCED_PARAMETER(Work);
BOOL bRet = FALSE;
{
_tprintf(_T("MyWorkCallback: Task performed.\n"));
}
return;
}
작업 객체에 설정될 콜백 함수입니다. 동일하게 간단한 출력 함수인데 중간에 hRet 변수와 스코프는 무슨 의미인지 모르겠네요. 차별점을 둔 것 같습니다.
InitializeThreadpoolEnvironment(&CallBackEnviron);
이제 다시 DemoCleanupPersistentWorkTimer 본문으로 돌아옵니다. 바로 스레드 풀 환경을 초기화시킵니다.
pool = CreateThreadpool(NULL);
if (NULL == pool) {
_tprintf(_T("CreateThreadpool failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 1;
SetThreadpoolThreadMaximum(pool, 1);
bRet = SetThreadpoolThreadMinimum(pool, 1);
if (FALSE == bRet) {
_tprintf(_T("SetThreadpoolThreadMinimum failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
그다음 스레드 풀을 생성합니다. 그리고 생성한 스레드 풀의 최소/최대 스레드 수량을 1로 설정합니다.
cleanupgroup = CreateThreadpoolCleanupGroup();
if (NULL == cleanupgroup) {
_tprintf(_T("CreateThreadpoolCleanupGroup failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 2;
생성한 객체들을 해제하기 위한 해제 그룹을 생성합니다.
SetThreadpoolCallbackPool(&CallBackEnviron, pool);
SetThreadpoolCallbackCleanupGroup(&CallBackEnviron,
cleanupgroup,
NULL);
생성한 스레드 풀을 환경에 설정합니다. 이후 설정된 환경을 해제 그룹과 연결합니다. 이제 스레드 풀을 사용할 준비를 마쳤습니다.
work = CreateThreadpoolWork(workcallback,
NULL,
&CallBackEnviron);
if (NULL == work) {
_tprintf(_T("CreateThreadpoolWork failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 3;
SubmitThreadpoolWork(work);
작업 객체를 생성합니다. 그다음 SubmitThreadpoolWork 함수를 통해 생성한 작업 객체를 스레드 풀에 할당합니다. 이때 스레드가 작업을 할당받아 간단한 출력문을 하나 출력할 것입니다.
timer = CreateThreadpoolTimer(timercallback,
NULL,
&CallBackEnviron);
if (NULL == timer) {
_tprintf(_T("CreateThreadpoolTimer failed. LastError: %u\n"),
GetLastError());
goto main_cleanup;
}
rollback = 4;
ulDueTime.QuadPart = (ULONGLONG) -(1 * 10 * 1000 * 1000);
FileDueTime.dwHighDateTime = ulDueTime.HighPart;
FileDueTime.dwLowDateTime = ulDueTime.LowPart;
SetThreadpoolTimer(timer,
&FileDueTime,
0,
0);
Sleep(1500);
타이머 객체를 생성하여 스레드 풀에 할당합니다. 중간에 있는 ulDueTime 변수를 통해 1초를 100 나노초(ns)로 변환합니다. 음수로 만든 이유는 현재 시간으로부터의 상대시간으로 계산하기 위해서입니다. 다음 대기 객체에 설정할 수 있게 FILETIME 구조체에 값을 전달합니다.
이후 SetThreadpoolTimer 함수를 통해 스레드 풀에 타이머 객체를 할당합니다. 첫 번째 매개변수는 사용할 대기 객체입니다. 두 번째 매개변수는 위에서 계산한 FILETIME 구조체 포인터입니다. 세 번째는 타이머의 주기입니다. 0 보다 크다면 주기적으로 콜백 함수를 호출합니다. 하지만 0이면 단발성으로 한 번 호출합니다. 네 번째는 콜백 함수를 호출하기 전에 지연할 수 있는 최대 시간입니다. 이 값을 설정하면 콜백 함수의 호출을 일괄적으로 처리합니다.
1초 뒤 타이머 객체의 콜백 함수의 출력문이 출력된 것을 확인할 수 있습니다.
CloseThreadpoolCleanupGroupMembers(cleanupgroup,
FALSE,
NULL);
rollback = 2;
goto main_cleanup;
main_cleanup:
switch (rollback) {
case 4:
case 3:
CloseThreadpoolCleanupGroupMembers(cleanupgroup,
FALSE, NULL);
case 2:
CloseThreadpoolCleanupGroup(cleanupgroup);
case 1:
CloseThreadpool(pool);
default:
break;
}
return;
}
마지막으로 사용했던 자원들을 해제합니다. 우선 CloseTheradpoolCleanupGroupMembers 함수를 통해 모든 객체를 해제하고 콜백 함수를 대기합니다. 그다음은 각가의 스레드 풀, 해제그룹을 해제합니다.
Output
MyWaitCallback: wait is over.
MyWaitCallback: wait is over.
MyWaitCallback: wait is over.
MyWaitCallback: wait is over.
MyWaitCallback: wait is over.
MyWorkCallback: Task performed.
MyTimerCallback: timer has fired.