Vista 及后续版本的新线程池

在上一篇的博文中,说了下老版本的线程池,在Vista之后,微软重新设计了一套线程池机制,并引入一组新的线程池API,新版线程池相对于老版本的来说,它的可控性更高,它允许程序员自己定义线程池,并规定线程池中的线程数量和其他一些属性。

线程池使用

线程池的使用主要需要下面的四步:
1. 创建工作项
2. 提交工作项
3. 等待工作项完成
4. 清理工作项
在前面说的四种线程池在使用上都是这4步,只是使用的API函数不同,每种线程池的每一步都有一个对应的API,总共有16个API

普通线程池

创建工作项的API为

PTP_WORK WINAPI CreateThreadpoolWork(
  __in         PTP_WORK_CALLBACK pfnwk,
  __inout_opt  PVOID pv,
  __in_opt     PTP_CALLBACK_ENVIRON pcbe
);

第一个参数是一个回调函数,当提交后,线程池中的线程会执行这个回调函数
第二个参数是传递给回调函数的参数
第三个参数是一个表示回调环境的结构,这个在后面会说
回调函数的原型

VOID CALLBACK WorkCallback(
  __inout      PTP_CALLBACK_INSTANCE Instance,
  __inout_opt  PVOID Context,
  __inout      PTP_WORK Work
);

第一个参数用于表示线程池当前正在处理的一个工作项的实例,在后面会说它怎么用
第二个参数是传给回调函数的参数的指针
第三个参数是当前工作项的结构
创建工作项完成之后调用SubmitThreadpoolWork将工作项提交到对应的线程池,由线程池中的线程处理这个工作项,该函数原型如下:

VOID WINAPI SubmitThreadpoolWork(
  __inout  PTP_WORK pwk
);

这个函数只有一个参数那就是工作项的指针,即我们想将哪个工作项提交。
提交工作项之后,在需要同步的地方,调用函数WaitForThreadpoolWorkCallbacks,等待线程池中的工作项完成,该函数原型如下

VOID WINAPI WaitForThreadpoolWorkCallbacks(
  __inout  PTP_WORK pwk,
  __in     BOOL fCancelPendingCallbacks
);

最后一个参数表示线程池是否需要执行未执行的工作项,注意它只能取消执行还没有开始执行的工作项,而不能取消已经有线程开始执行的工作项,
最后调用函数CloseThreadpoolWork清理工作项,该函数的原型如下:

VOID WINAPI CloseThreadpoolWork(
  __inout  PTP_WORK pwk
);

就我个人的理解,TP_WORK应该保存的是一个工作项的信息,包含工作项的回调以及传递个回调函数的参数,每当提交一个工作项就是把这个结构放入到线程池的队列中,当线程池中有空闲线程的时候从队列中取出这个结构,将结构中的回调函数参数传递给回调函数,并调用它。我们可以重复提交同一个工作项多次,但是每个工作项一旦定义好了,那么传递给对应回调函数的参数应该是固定的,后期是没办法更改它的。它的等待函数调用时根据第二个参数,如果为TRUE则将线程池队列中的工作项清除,然后等待所有线程都为空闲状态时返回,而当参数为FALSE时,就不对队列中的工作项进行操作,并且一直等到线程池中的所有线程为空闲。
下面是一个具体的使用例子:

VOID CALLBACK MyWorkCallback(
                             PTP_CALLBACK_INSTANCE Instance,
                             PVOID                 Parameter,
                             PTP_WORK              Work
                             )
{
    int nWaitTime = 4;
    printf("线程[%04x]将等待%ds
", GetCurrentThreadId(), nWaitTime);
    Sleep(nWaitTime * 1000);
    printf("线程[%04x]执行完毕
", GetCurrentThreadId());
}

int _tmain(int argc, _TCHAR* argv[])
{

    PTP_WORK_CALLBACK workcallback = MyWorkCallback;
    PTP_WORK work = CreateThreadpoolWork(workcallback, NULL, NULL); //创建工作项

    for (int i = 0; i < 4; i++)
    {
        SubmitThreadpoolWork(work); //提交工作项
    }

    //等待线程池中的所有工作项完成
    WaitForThreadpoolWorkCallbacks(work, FALSE);
    //关闭工作项
    CloseThreadpoolWork(work);
    return 0;
}

定时器线程池

定时器线程池中使用的对应的API分别为CreateThreadpoolTimer、SetThreadpoolTimer、WaitForThreadpoolTimerCallbacks和CloseThreadpoolTimer,这些函数的参数与之前的函数参数基本类似,区别比较大的是SetThreadpoolTimer,由于涉及到定时器,所以这里的参数稍微复杂一点

VOID WINAPI SetThreadpoolTimer(
  __inout   PTP_TIMER pti,
  __in_opt  PFILETIME pftDueTime,
  __in      DWORD msPeriod,
  __in_opt  DWORD msWindowLength
);

第二个参数表示定时器触发的时间,它是一个64位的整数,如果为正数表示一个绝对的时间,表示从1960年到多少个100ns的时间后触发,如果为负数则表示从设置之时起经过多少时间后触发,单位为微秒(转化为秒是1000 * 1000)
第三个参数每隔多长时间触发一次,如果只是想把这个定时器作为一次性的,和第四个参数没有用处,而如果想让线程池定期的触发它,这个值就是定期触发的间隔 时间,单位为毫秒
第四个参数是用来给回调函数的执行时机增加一定的随机性,如果这个定时器是一个定期触发的定时器,那么这个值告诉线程池,可以在自定时器设置时间起,在(msPeriod - msWindowLength, mePeriod + msWindowsLong)这个区间之后的任意时间段触发
另外我自己在编写测试代码的时候发现有的时候调用WaitForThreadpoolTimerCallbacks可能立即就返回了,后来我自己分析可能的原因是这个函数会在线程池队列中没有需要处理的工作项,并且线程池中线程为空闲的时候返回,当我使用定时器的时候,在等待时可能这个时候定时器上的时间未到,而线程池中又没有需要处理的定时器的工作项,所以它就返回了从而未达到等待的效果。
下面是一个使用的具体例子,这个例子是《Windows核心编程》这本书中的例子,我觉得它里面有一个更改MessageBox显示信息的功能,所以将其修改了下作为例子

int g_nWaitTime = 10;
TCHAR g_szTitle[] = _T("提示");
#define ID_MSGBOX_STATIC_TEXT    0x0000ffff //MessageBox上内容部分的控件ID

VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer)
{
    HWND hWnd = FindWindow(NULL, g_szTitle); //找到MessageBox所对应的窗口句柄
    if (NULL != hWnd)
    {
        TCHAR szText[1024] = _T("");
        StringCchPrintf(szText, 1024, _T("您将有%ds的时间"), --g_nWaitTime);
        SetDlgItemText(hWnd, ID_MSGBOX_STATIC_TEXT, szText); //更改显示信息
    }

    if (g_nWaitTime == 0)
    {
        ExitProcess(0);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    //创建定时器历程
    PTP_TIMER pTimer = CreateThreadpoolTimer(TimerCallback, NULL, NULL);
    //将定时器历程加入到线程池
    ULARGE_INTEGER uDueTime = {0};
    FILETIME FileDueTime = {0};
    uDueTime.QuadPart = (LONGLONG) -(1 * 10 * 1000 * 1000); //时间为1s
    FileDueTime.dwHighDateTime = uDueTime.HighPart;
    FileDueTime.dwLowDateTime = uDueTime.LowPart;
    SetThreadpoolTimer(pTimer, &FileDueTime, 1000, 0); //每1s调用一次
    WaitForThreadpoolTimerCallbacks(pTimer, FALSE); //此处调用等待函数会立即返回

    TCHAR szText[] = _T("您将有10s的时间");
    MessageBox(NULL, szText, g_szTitle, MB_OK);

    //关闭工作项
    CloseThreadpoolTimer(pTimer);
    return 0;
}

同步对象线程池

对这种线程池的使用主要调用这样几个函数: CreateThreadpoolWait、SetThreadpoolWait、WaitForThreadpoolWaitCallbacks、CloseThreadpoolWait ,这几个函数的使用与之前的普通线程池的使用类似,在这就不再进行说明直接给例子

VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
{
    if (WaitResult == WAIT_OBJECT_0)
    {
        printf("[%04x] wait the event
", GetCurrentThreadId());
    }else if (WaitResult == WAIT_TIMEOUT)
    {
        printf("[%04x] time out
", GetCurrentThreadId());
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    //创建等待线程池
    PTP_WAIT pWait = CreateThreadpoolWait(WaitCallback, NULL, NULL);

    //创建事件
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

    //等待时间为1s
    FILETIME ft = {0};
    ULARGE_INTEGER uWaitTime = {0};
    uWaitTime.QuadPart = (LONGLONG) - 1 * 1000 * 1000;
    ft.dwHighDateTime = uWaitTime.HighPart;
    ft.dwLowDateTime = uWaitTime.LowPart;

    for (int i = 0; i < 5; i++)
    {
        //模拟等待5次
        SetThreadpoolWait(pWait, hEvent, &ft);
        Sleep(1000); //休眠
        SetEvent(hEvent);
    }

    WaitForThreadpoolWaitCallbacks(pWait, FALSE);
    CloseThreadpoolWait(pWait);
    CloseHandle(hEvent);
    return 0;
}

这种类型的回调函数的WaitResult参数实际上是一个DWORD类型,表示调用这个回调的原因,WAIT_OBJECT_0表示同步对象变为有信号,WAIT_TIMEOUT表示超时WAIT_ABANDONED_0表示穿入的互斥量被遗弃(只有在同步对象为互斥量的时候才会有这种值)

完成端口线程池

完成端口线程池的使用主要用这些API:CreateThreadpoolIo、StartThreadpoolIo、WaitForThreadpoolIoCallbacks、CloseThreadpoolIo,这些函数的使用也是十分的简单,下面再次将之前的完成端口写日志的例子进行改写:

int _tmain(int argc, _TCHAR* argv[])
{
    TCHAR szAppPath[MAX_PATH] = _T("");
    GetAppPath(szAppPath);
    StringCchCat(szAppPath, MAX_PATH, _T("NewIocpLog.txt"));
    HANDLE hFile = CreateFile(szAppPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        return 0;
    }

    //创建IOCP线程池
    g_pThreadpoolIO = CreateThreadpoolIo(hFile, IoCompletionCallback, hFile, NULL);
    StartThreadpoolIo(g_pThreadpoolIO);

    //写入Unicode字节码
    LPIOCP_OVERLAPPED pIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED));
    pIocpOverlapped->dwDataLen = sizeof(WORD);
    pIocpOverlapped->hFile = hFile;
    WORD dwUnicode = MAKEWORD(0xff, 0xfe); //构造Unicode前缀
    pIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD));
    CopyMemory(pIocpOverlapped->pData, &dwUnicode, sizeof(WORD));
    //偏移文件指针
    pIocpOverlapped->Overlapped.Offset = g_FilePointer.LowPart;
    pIocpOverlapped->Overlapped.OffsetHigh = g_FilePointer.HighPart;
    g_FilePointer.QuadPart += pIocpOverlapped->dwDataLen;

    //写文件
    WriteFile(hFile, pIocpOverlapped->pData, pIocpOverlapped->dwDataLen, &pIocpOverlapped->dwWrittenLen, &pIocpOverlapped->Overlapped);

    //创建线程进行写日志操作
    HANDLE hWrittenThreads[MAX_WRITE_THREAD];
    for (int i = 0; i < MAX_WRITE_THREAD; i++)
    {
        hWrittenThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThread, &hFile, 0, NULL);
    }

    //等待所有写线程执行完成
    WaitForMultipleObjects(MAX_WRITE_THREAD, hWrittenThreads, TRUE, INFINITE);
    for (int i = 0; i < MAX_WRITE_THREAD; i++)
    {
        CloseHandle(hWrittenThreads[i]);
    }

    //等待线程池中待处理的IO完成请求
    WaitForThreadpoolIoCallbacks(g_pThreadpoolIo, FALSE);
    CloseHandle(hFile);
    //关闭IOCP线程池
    CloseThreadpoolIo(g_pThreadpoolIO);

    return 0;
}

VOID CALLBACK WriteThread(LPVOID lpParam)
{
    TCHAR szBuf[255] = _T("线程[%04x]模拟写入一条日志记录
");
    TCHAR szWrittenBuf[255] = _T("");
    StringCchPrintf(szWrittenBuf, 255, szBuf, GetCurrentThreadId());

    for (int i = 0; i < EVERY_THREAD_WRITTEN; i++)
    {
        //提交一个IOCP历程
        StartThreadpoolIo(g_pThreadpoolIO);
        LPIOCP_OVERLAPPED lpIocpOverlapped = (LPIOCP_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(IOCP_OVERLAPPED));
        size_t dwBufLen = 0;
        StringCchLength(szWrittenBuf, 255, &dwBufLen);
        lpIocpOverlapped->dwDataLen = dwBufLen * sizeof(TCHAR);
        lpIocpOverlapped->pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (dwBufLen + 1) * sizeof(TCHAR));
        CopyMemory(lpIocpOverlapped->pData, szWrittenBuf, dwBufLen * sizeof(TCHAR));

        lpIocpOverlapped->hFile = *(HANDLE*)lpParam;
        //同步文件指针
        *((LONGLONG*)&(lpIocpOverlapped->Overlapped.Pointer)) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + lpIocpOverlapped->dwDataLen, g_FilePointer.QuadPart);

        //写文件
        WriteFile(lpIocpOverlapped->hFile, lpIocpOverlapped->pData, lpIocpOverlapped->dwDataLen, &lpIocpOverlapped->dwWrittenLen, &lpIocpOverlapped->Overlapped);
    }
}

VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PVOID Overlapped,ULONG IoResult,ULONG_PTR NumberOfBytesTransferred,PTP_IO Io)
{
    LPIOCP_OVERLAPPED pIOCPOverlapped = (LPIOCP_OVERLAPPED)Overlapped;

    //释放对应的内存空间
    printf("线程[%04x]得到IO完成通知,写入长度%d
", GetCurrentThreadId(), pIOCPOverlapped->dwDataLen);
    if (pIOCPOverlapped->pData != NULL)
    {
        HeapFree(GetProcessHeap(), 0, pIOCPOverlapped->pData);
    }

    if (NULL != pIOCPOverlapped)
    {
        HeapFree(GetProcessHeap(), 0, pIOCPOverlapped);
        pIOCPOverlapped = NULL;
    }
}

在新版的完成端口的线程池中,每当需要进行IO操作时,要保证在IO操作之前调用StartThreadpoolIo提交请求。如果没有那么我们的回调函数将不会被执行。
注意:后面两种线程池与旧版的相比,最大的区别在于新版的是一次性的,也就是每提交一次,它只会执行一次,要想让其不停触发就需要不停的进行提交,而旧版的只需要绑定,一旦相应的事件发生,他就会不停地的执行

线程池控制

回调函数的终止操作

线程池提供了一种便利的方法,用来描述当我们的回调函数返回之后,应该执行的一些操作,通过这种方式,可以通知其他线程,回调函数已经执行完毕。通过调用下面的一些API可以设置对应的同步对象,在线程池外的其他线程等待同步对象就可以知道什么时候回调执行完毕

函数 终止操作
LeaveCriticalWhenCallbackReturns 当回调函数返回时,线程池会自动调用LeaveCritical,并在参数中传入指定的CRITICAL_SECTION结构
ReleaseMutexWhenCallbackReturns 当回调函数返回时,线程池会自动调用ReleaseMutexWhen并在参数中传入指定的HANDLE
ReleaseSemaphoreWhenCallbackReturns 当回调函数返回时,线程会自动调用ReleaseSemphore并在参数中传入指定的HANDLE
SetEventWhenCallbackReturns 当回调函数返回时,线程会自动调用SetEvent,并在参数中传入指定的HANDLE
FreeLibraryWhenCallbackReturns 当回调函数返回时,线程会自动调用FreeLibrary并在参数中传入指定的HANDLE

前4个函数给我们提供了一种方式来通知另外一个线程,回调函数调用完成,而最后一个函数则提供了一种在回调函数调用完成之时,清理动态库的方式,如果回调函数是在dll中实现的,但是在回调函数结束之时,我们希望卸载这个dll,这个时候不能调用FreeLibrary,这个时候回调函数虽然完成了任务,但是在后面还有函数栈平衡的操作,如果在返回时,我们将dll从内存中卸载,必然会导致最后的栈平衡操作访问非法内存,从而时应用程序崩溃。但是我们可以调用FreeLibraryWhenCallbackReturns,完成这个任务。
下面是一个具体的例子:

typedef struct tagWAIT_STRUCT
{
    HANDLE hEvent;
    DWORD dwThreadId;
}WAIT_STRUCT, *LPWAIT_STRUCT;

WAIT_STRUCT g_waitStruct = {0};

VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
    g_waitStruct.dwThreadId = GetCurrentThreadId();

    Sleep(1000 * 10);
    SetEventWhenCallbackReturns(Instance, *(HANDLE*)&g_waitStruct);
}

int _tmain(int argc, _TCHAR* argv[])
{
    PTP_WORK pWork = CreateThreadpoolWork(WorkCallback, NULL, NULL);
    g_waitStruct.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    SubmitThreadpoolWork(pWork);

    WaitForSingleObject(g_waitStruct.hEvent, INFINITE);
    printf("线程池中线程[%04x]执行完成
", g_waitStruct.dwThreadId);

    CloseThreadpoolWork(pWork);
    return 0;
}

上面的代码首先创建一个无信号的event对象,然后在回调函数中调用SetEventWhenCallbackReturns,当回调函数完成之时就会将event设置为有信号,这样我们在主线程中就可以等待,一旦回调函数执行完成,event变为有信号,wait函数就会返回。同时我们定义一个结构体尝试着从线程池中带出一个线程ID,并在主线程中使用它

对线程池进行定制

上面在讨论四种线程池的时候,使用的都是系统自带的线程池,这些线程池由系统管理,我们只能使用,而不能对它们的一些属性进行定制,但是新版本的线程池中提供了这样的方式,要对线程池进行定制,不能使用系统已经定义好的线程池,得自己定义,定义线程池使用API函数CreateThreadPool,这个函数只有一个参数,这个参数是Windows的保留参数目前应该赋值为NULL。该函数会返回一个PTP_POOL 类型的值,这个值是一个指针,用来标识一个线程池。
创建完成之后,我们可以函数SetThreadpoolThreadMaximum 或者SetThreadpoolThreadMinimum来规定线程池中的最大和最小线程。
当不需要自定义的线程池的时候可以使用函数CloseThreadPool,来清理自定义线程池。

线程池的回调环境

线程池的回调环境规定了回调函数的执行环境,比如由哪个线程池中的线程来调用,对应线程池的版本,对应的清理器和其他的属性等等。环境的结构定义如下:

typedef struct _TP_CALLBACK_ENVIRON {
    TP_VERSION                         Version; //线程池的版本
    PTP_POOL                           Pool; //关联的线程池
    PTP_CLEANUP_GROUP                  CleanupGroup; //对应的环境清理组
    PTP_CLEANUP_GROUP_CANCEL_CALLBACK  CleanupGroupCancelCallback;
    PVOID                              RaceDll;
    struct _ACTIVATION_CONTEXT        *ActivationContext;
    PTP_SIMPLE_CALLBACK                FinalizationCallback;
    union {
        DWORD                          Flags;
        struct {
            DWORD                      LongFunction :  1;
            DWORD                      Private      : 31;
        } s;
    } u;
} TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON;

虽然这个结构微软对外公布,而且是可以在程序中直接使用的,但是最好不要这么做,我们应该使用它提供的API对其进行操作,首先可以调用InitializeThreadpoolEnvironment来创建一个对应的回调环境,对我们传入的TP_CALLBACK_ENVIRON变量进行初始化。然后可以调用函数SetThreadpoolCallbackPool来规定由哪个线程池来调用对应的回调函数,如果将参数ptpp传入NULL,则使用系统默认的线程池。
另外还可以调用SetThreadpoolCallbackRunsLong 来告诉线程池,我们的任务需要较长的时间来执行。最后当我们不需要这个回调环境的时候可以使用函数DestroyThreadpoolEnvironment来清理这个结构。
我自己在看这一块的时候很长时间都转不过弯来,总觉得回调环境是由线程池持有的,每个线程池都有自己的回调环境,其实这个是错误的,既然它叫做回调环境,自然与线程池无关,它是用来控制回调行为的。当我们在创建对应的任务时,最后一个参数就是回调环境的指针,在提交任务时会首先将任务提交到回调环境所规定的线程池中,由对应的线程池来处理。函数SetThreadpoolCallbackPool从表面意思来看是未线程池设置一个回调环境其实这个意思正好相反,是为某个回调指定对应调用的线程池。在后面就可以看到,回调环境可比线程池大的多

线程池的清理组

为了得体的销毁自定义的线程池(系统自定义线程池不会被销毁),我们需要知道系线程池中各个任务何时完成,只有当所有任务都完成时销毁线程池才算得体的销毁,只有这样才能顺利的清理相关资源。但是由于线程池中的各项任务可能由不同的线程提交,提交的时机,任务执行完所需要的时间各不相同,所以基本上不可能知道线程池中的任务何时完成。为了解决这个问题,新版的线程池提供了清理组的概念。
TP_CALLBACK_ENVIRON结构的PTP_CLEANUP_GROUP就为对应的执行环境绑定了一个清理组。当线程池中的任务都处理完成时能够得体的清理线程池
可以调用CreateThreadpoolCleanupGroup来创建一个清理组,然后调用SetThreadpoolCallbackCleanupGroup来将线程池与对应的清理组。它的原型如下:

VOID SetThreadpoolCallbackCleanupGroup(
  __inout   PTP_CALLBACK_ENVIRON pcbe,
  __in      PTP_CLEANUP_GROUP ptpcg,
  __in_opt  PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);

第一个参数是一个回调环境
第二个参数是一个对应的清理组,这两个参数就将对应的回调环境和清理组关联起来
第三个参数是一个回调函数,每当一个工作项被取消,这个函数将会被调用。
对应的回调函数的原型如下:

VOID NTAPI CleanupGroupCancelCallback(PVOID pvObjectContext, PVOID CleanupContext);

每当创建一个任务时,如果最后一个参数不为NULL,那么对应的清理组中会增加一项,表示又增加一个需要潜在清理的任务。最后我们调用对应的清理工作项的函数时,相当于显示的将需要清理的项从对应的清理组中去除。当我们的应用程序想要销毁线程池时,调用函数CloseThreadpoolCleanupGroupMembers。这个函数相比于之前的WaitForThreadpoolTimerCallbacks来说,它可以等待线程池中的所有工作项,而不管工作项是哪种类型,而对应的wait函数只能等待对应类型的工作项。

VOID WINAPI CloseThreadpoolCleanupGroupMembers(
  __inout      PTP_CLEANUP_GROUP ptpcg,
  __in         BOOL fCancelPendingCallbacks,
  __inout_opt  PVOID pvCleanupContext
);

CloseThreadpoolCleanupGroupMembers函数的第二个参数也是一个BOOL类型,它的作用与对应的wait函数中第二个参数的作用相同。如果第二个参数设置为NULL,那么每当该函数取消一个工作项,对应的PTP_CLEANUP_GROUP_CANCEL_CALLBACK 类型的回调就要被调用一次
CleanupGroupCancelCallback函数中第一个参数是被取消项的上下文,这个上下文是由对应的创建工作项的函数的pvContext参数传递进来的,而第二个参数是由CloseThreadpoolCleanupGroupMembers函数的第三个参数传递进来的。
当所有的工作项被取消后调用CloseThreadpoolCleanupGroup来释放清理组所占的资源。
最后调用DestroyThreadpoolEnviroment和CloseThreadPool这样就可以得体的关闭线程池
下面是使用的一个例子:

VOID NTAPI CleanupGroupCancelCallback(PVOID pvObjectContext, PVOID CleanupContext)
{
    printf("有任务[%d][%d]被取消
", *(int*)pvObjectContext, *(int*)CleanupContext);
}

VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer)
{
    Sleep(1000);
    printf("有对应的定时器历程被调用
");
}
int _tmain(int argc, _TCHAR* argv[])
{
    TP_CALLBACK_ENVIRON environ = {0}; //创建回调环境
    InitializeThreadpoolEnvironment(&environ);
    PTP_CLEANUP_GROUP pCleanUp = CreateThreadpoolCleanupGroup(); //创建清理组
    PTP_POOL pool = CreateThreadpool(NULL); //创建自定义线程池

    //设置线程池中的最大、最小线程数
    SetThreadpoolThreadMinimum(pool, 2);
    SetThreadpoolThreadMaximum(pool, 8);

    //设置对应的回调环境和清理组
    SetThreadpoolCallbackPool(&environ, pool);
    SetThreadpoolCallbackCleanupGroup(&environ, pCleanUp, CleanupGroupCancelCallback);

    //创建对应的工作项
    int i = 1;
    PTP_TIMER pTimerWork = CreateThreadpoolTimer(TimerCallback, &i, &environ);

    ULARGE_INTEGER uDueTime = {0};
    FILETIME ft = {0};
    uDueTime.QuadPart = (LONGLONG) - 10 * 1000 *1000; //设置时间为10s
    ft.dwHighDateTime = uDueTime.HighPart;
    ft.dwLowDateTime = uDueTime.LowPart;

    SetThreadpoolTimer(pTimerWork, &ft, 10 * 1000, 0);

    //休眠1s保证定时器历程被提交
    Sleep(1000);
    int j = 2;

    //等待所有历程执行完成,并清理资源
    CloseThreadpoolCleanupGroupMembers(pCleanUp, TRUE, &j);

    CloseThreadpoolCleanupGroup(pCleanUp);
    DestroyThreadpoolEnvironment(&environ);

    CloseThreadpool(pool);
    return 0;
}

上面的例子中,首先定义了一个回调环境并进行初始化,然后定义自定义线程和对应的清理环境,并将他们绑定。并且在定义清理器时指定对应的回调函数。
接着又定义了一个定时器线程并给一个上下文。
然后提交这个定时器历程。为了保证能顺利提交,在主程序中等待1s。
最后我们直接取消它,由于定时器触发的时间为10s这个时候肯定还没有执行,而根据之前说的,当我们取消一个已提交但是未执行的工作项时会调用对应的清理组规定的回调,这个时候CleanupGroupCancelCallback会被调用。它的参数的值分别由CreateThreadpoolTimer和CloseThreadpoolCleanupGroupMembers给出,所以最终输出结果如下:
这里写图片描述
自定义线程池可以很方便的控制它的行为。但是为了要得体的清理它所以得加上一个清理组,最终当我们使用自定义线程池时,基本步骤如下:
1. 调用函数InitializeThreadpoolEnvironment初始化一个回调环境
2. 调用CreateThreadpoolCleanupGroup创建一个清理组,并根据需要给出对应的清理回调
3. 调用CreateThreadpool创建自定义线程池
4. 调用对应的函数,设置自定义线程池的相关属性
5. 调用函数SetThreadpoolCallbackPool将线程池与回调环境绑定
6. 调用函数SetThreadpoolCallbackCleanupGroup将回调环境与对应的清理组绑定
7. 调用对应的函数创建工作项,并提交
8. 调用函数CloseThreadpoolCleanupGroupMembers等待清理组中的所有工作项被执行完或者被取消
9. 调用CloseThreadpoolCleanupGroup关闭清理组并释放资源
10. 调用DestroyThreadpoolEnvironment清理回调环境
11. 调用CloseThreadpool函数关闭自定义的线程池
使用清理组的方式清理工作项相比于调用对应的close函数清理工作项来说,显得更方便,一来自定义线程池中工作项的种类繁多,每个工作项都调用一个Close函数显得太复杂,而且当工作项过多时,不知道何时哪个工作项执行完,这个时候如果强行调用函数关闭工作项,显得有点暴力,所以用工作组的方式更为优雅一些

原文地址:https://www.cnblogs.com/lanuage/p/7725681.html