线程池
(本章节中样例都是用 VS2010 编译调试的)
线程池编写必须在 Windows Vista 操作系统(以及以上版本号的操作系统)下,且 C++ 编译器版本号至少是 VS2008
线程池的功能
- 以异步的方式来调用一个函数
- 每隔一段时间调用一个函数
- 当内核对象触发的时候调用一个函数
- 当异步 I/O 请求完毕的时候调用一个函数
注意
当一个进程初始化的时候,它并没有不论什么与线程池的开销.可是,一旦调用了新的线程池函数,系统就会为进程对应的内核资源,当中的一些资源在进程终止之前都将一直存在.正如我们能够看到,使用线程池的开销取决于使用方法:系统会以进程的名义来分配线程,其它内核以及内部数据结构.因此我们不应该盲目地使用这些线程池函数,而是必须慎重地考虑,这些函数能做什么,以及它们不能做什么.
在线程池编程中,我们从来不须要自己调用 CreateThread.系统会自己主动为我们的进程创建线程,并在规定的条件下让线程池中的线程调用我们的回调函数.此外,这个线程在处理完毕一个客户请求后,它不会立马被销毁,而是回到线程池,准备优点理队列中的不论什么其它工作项,线程池会不断地反复使用当中的线程,而不会频繁地创建销毁线程,相应用程序来说,这样能够显著地提升性能,由于创建和销毁线程会消耗大量的时间.当然,假设线程池检測到创建的还有一个线程将能更好地为应用程序服务,那么它会这样做.假设线程池检測到它的线程数量已经供过于求,那么它会销毁当中一些线程.除非我们很清楚自己在做什么,否则的话不妨相信线程内部的算法,让它自己主动地相应用程序的工作量进行微调.
默认线程池,在进程存在期间它不会被销毁.生命周期与进程同样.在进程终止的时候,Windows 会将其销毁并负责全部的清理工作.
对线程池的制定
能够用 CreateThreadpool 来创建一个新的线程池,该函数返回一个 PTP_POOL 值,表示新创建的线程池.接着我们能够调用后面两个函数来设置线程池中线程的最大数量和最小数量 SetThreadpoolThreadMinimum / SetThreadpoolThreadMaximum 线程池始终保持池中的线程数量至少是指定的最小数量,并同意线程数量增长到指定的最大数量,顺便一提,默认线程池的最小数量为1,最大数量为500.然后在引用程序不在须要自己定义线程池时,应调用 CloseThreadpool 将其销毁.在调用这个函数后,我们将无法在将不论什么新的项加入到线程池的队列中.线程池中当前正在处理的队列中的线程会完毕它们的处理并终止.此外,线程池的队列中全部尚未開始处理的项将被取消.
一旦我们创建了自己的线程池,并制定了线程池的最小数量和最大数量,我们就能够初始化一个回调环境,它包括了一些课应用于工作项的额外的设置或配置.(线程池回调环境结构 _TP_CALLBACK_ENVIRON ,其定义在 WinNT.h )
/*************************************** 显示此结果的编译环境为 VS2010 ***************************************/ #if (_WIN32_WINNT >= _WIN32_WINNT_WIN7) typedef struct _TP_CALLBACK_ENVIRON_V3 { 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 Persistent : 1; DWORD Private : 30; } s; } u; TP_CALLBACK_PRIORITY CallbackPriority; DWORD Size; } TP_CALLBACK_ENVIRON_V3; typedef TP_CALLBACK_ENVIRON_V3 TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON; #else typedef struct _TP_CALLBACK_ENVIRON_V1 { 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 Persistent : 1; DWORD Private : 30; } s; } u; } TP_CALLBACK_ENVIRON_V1; typedef TP_CALLBACK_ENVIRON_V1 TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON; #endif
然后我们能够调用 InitializeThreadpoolEnvironment 初始化这个结构体,接着当然回调环境必须调用 SetThreadpoolCallbackPool 标明给工作项应该由哪个线程池来处理.最后当我们不在须要这个使用线程池回调环境的时候,应该调用 DestroyThreadpoolEnvironment 来对它进行清理工作.
-
- 然而能够调用 SetThreadpoolCallbackRunsLong 函数来告诉回调环境,工作项通常须要较长的时间来处理.这使得线程池会更快地创建线程,其目的是为了尝试在对工作项进行处理的时候,以一种更为公平的方式来替代最有效的方式.
- 也能够调用 SetThreadpoolCallbackLibrary 来确保仅仅要线程池中还有待处理的工作项,就将一个特定的 DLL 一直保持在进程空间中.基本上 SetThreadpoolCallbackLibrary 函数的存在目的是为了消除潜在的竞态条件(race condition),从而避免可能导致死锁.这个相当高级的特性,更具体信息參阅 Platform SDK 文档.
线程池的销毁(清理组)
为了得体地销毁私有线程池,我们首先能够须要通过调用 CreateThreadpoolCleanupGroup 来创建一个清理组,然后再将这个清理组与一个以绑定到线程池的回调函数结构体 TP_CALLBACK_ENVIRON 调用 SetThreadpoolCallbackCleanupGroup 函数把两者关联起来.当中 SetThreadpoolCallbackCleanupGroup 的 pfng 參数标识一个回调函数的地址(函数原型 即CleanupGroupCancelCallback),假设传给 pfng 參数值不为 NULL ,且当清理组被取消时那么这个回调函数会被调用.
当我们调用 CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait 或 CreateThreadpoolIo 的时候,假设最后那个參数,即指向 PTP_CALLBACK_ENVIRON 结构体指针,不等于 NULL,那么所创建的项会被加入到相应的回调环境的清理组中,其目的是为了表示有线程池中加入了一项,须要潜在清理.在这些对了项完毕后,假设我们调用 CloseThreadpoolWork, CloseThreadpoolTimer, CloseThreadpoolWait 和 CloseThreadpoolIo, 那就等于是隐式将相应的项从组中移除.
最后,在程序想要销毁线程池的时候,调用 CloseThreadpoolCleanupGroupMembers.这个函数与以下的 WaitForThreadpool*(例: WaitForThreadpoolWork)函数相似.当线程调用 CloseThreadpoolCleanupGroupMembers 时候,函数会一直等待,知道线程池的工作组中全部剩余的项(即已经创建当尚未关闭的项)都已经处理完成为止.调用者还能够传 TRUE 给 fCancelPendingCallbacks 參数.这样会将全部已提交但尚未处理的工作项直接取消,函数会在全部当前正在执行的工作项王城之后返回.假设传给 fCancelPendingCallbacks 參数为 TRUE,并且传给 SetThreadpoolCallbackCleanupGroup 的 pfng 參数值是一个 CleanupGroupCancelCallback 函数地址,那么对每个被取消的工作项,我们的回调函数会被调用,在 CleanupGroupCancelCallback 函数中.參数 ObjectContext 会包括每一个被取消的上下文.(该上下文信息是通过 CreateThreadpool* 函数的 pv 參数设置的)在 CleanupGroupCancelCallback 函数中, 參数 CleanupContext 包括的上下文是通过 CloseThreadpoolCleanupGroupMembers 函数的 pvCleanupContext 參数传入的.假设在调用 CloseThreadpoolCleanupGroupMembers 时传入 FALSE 给 fCancelPendingCallbacks 參数.那么在返回之前,线程池会发时间来处理队列中全部剩余的项.注意这样的情况下我们的 CleanupGroupCancelCallback 函数绝对不会被调用.因此能够传 NULL 给 pvCleanupContext 參数.
线程池制定与销毁代码例子
编写步骤
程序源代码
#include<Windows.h> #include<iostream> #include<cstdlib> using namespace std; VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context); void main() { PTP_POOL tPool; TP_CALLBACK_ENVIRON pcbe; //创建线程池 tPool = CreateThreadpool(NULL); //设置线程池最大最小的线程数量 SetThreadpoolThreadMinimum(tPool,1); SetThreadpoolThreadMaximum(tPool,2); //初始化线程池环境变量 InitializeThreadpoolEnvironment(&pcbe); //为线程池设置线程池环境变量 SetThreadpoolCallbackPool(&pcbe,tPool); //单次工作提交 TrySubmitThreadpoolCallback(SimpleCallback,NULL,&pcbe); system("pause"); //清理线程池的环境变量 DestroyThreadpoolEnvironment(&pcbe); //关闭线程池 CloseThreadpool(tPool); } VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context) { cout<<"this is SimpleCallback function!"<<endl; }
执行结果
以异步的方式调用函数
相关函数
- TrySubmitThreadpoolCallback (向线程池提交工作请求函数<一次性>,回调函数原型)
- CreateThreadpoolWork (为线程池创建一个提交工作的工作对象,回调函数原型)
绝对不要让回调函数调用 WaitForThreadpoolWork 并将自己的工作项作为參数传入,由于这样会导致死锁. - SubmitThreadpoolWork (想线程池提交工作请求函数<非一次性>)
- WaitForThreadpoolWorkCallbacks (取消已提交但未运行的工作项
/ 等待工作项处理完毕把自己挂起)
当中的 fCancelPendingCallbacks 參数
若为 true ,函数会试图取消先前提交的那个工作项.假设线程池中的线程正在处理那个工作项,那么该过程不会被打断,函数会一直等到该工作项已经被处理完毕后在返回.
若为 false ,那么函数会将调用线程挂起,知道指定工作项的处理已经完毕并且线程池中处理该工作项的线程也已经被回收并准备处理下一个工作项.
假设用一个 PTP_WORK 对象已经提交了非常多工作项,并且传给 fCancelPendingCallbacks 參数为 false,那么 WaitForThreadpoolWorkCallbacks 会等待线程池处理全然部提交的工作项.假设传给 fCancelPendingCallbacks 为 true,那么 WaitForThreadpoolWorkCallbacks 仅仅会等到当前正在执行的工作项完毕为止. - CloseThreadpoolWork (取消能够多次提交工作的工作对象)
编写步骤
代码例子
程序源代码
#include<Windows.h> #include<iostream> #include<cstdlib> using namespace std; VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context); VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work); void main() { PTP_WORK tpWork; //单次工作提交 TrySubmitThreadpoolCallback(SimpleCallback,NULL,NULL); //创建工作对象 tpWork = CreateThreadpoolWork(WorkCallback,NULL,NULL); //提交工作 SubmitThreadpoolWork(tpWork); SubmitThreadpoolWork(tpWork); //等待工作结束 WaitForThreadpoolWorkCallbacks(tpWork,false); //关闭工作对象 CloseThreadpoolWork(tpWork); system("pause"); } VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context) { cout<<"this is SimpleCallback function!"<<endl; } VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work) { cout<<"this is WorkCallback function!"<<endl; }
执行结果
以时间段来调用函数
相关函数
- CreateThreadpoolTimer (为线程池创建一个定时调用的工作对象,回调函数原型)
绝对不要让回调函数调用 WaitForThreadpoolTimer 并将自己的工作项作为參数传入,由于这样会导致死锁. - SetThreadpoolTimer (为线程池的定时调用工作对象注冊计时器,也能够改动以注冊的定时调用工作对象的计时器,调用方式有些类似可等待计时器对象)
- WaitForThreadpoolTimerCallbacks (取消已提交但未运行的定时工作项 / 等待定时工作项处理完毕把自己挂起)
- CloseThreadpoolTimer (取消定时调用的工作对象)
- IsThreadpoolTimerSet (推断定时调用的工作对象是否设置过计时器)
编写步骤
代码例子
程序源代码
#include<Windows.h> #include<iostream> #include<cstdlib> using namespace std; VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_TIMER Timer); void main() { PTP_TIMER tpTimer; FILETIME liDueTime; LARGE_INTEGER liUTC; liUTC.QuadPart = -30000000; liDueTime.dwLowDateTime = liUTC.LowPart; liDueTime.dwHighDateTime = liUTC.HighPart; //创建定时调用的工作对象 tpTimer = CreateThreadpoolTimer(TimerCallback,NULL,NULL); //推断定时调用的工作对象是否注冊过计时器 if(!IsThreadpoolTimerSet(tpTimer)) { //为定时调用工作对象注冊计时器 SetThreadpoolTimer(tpTimer,&liDueTime,0,0); } //睡眠主进程,等待计时器加入工作 Sleep(4000); //等待定时调用的工作对象 WaitForThreadpoolTimerCallbacks(tpTimer,false); //关闭定时调用的工作对象 CloseThreadpoolTimer(tpTimer); system("pause"); } VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_TIMER Timer) { cout<<"this is TimerCallback function!"<<endl; }
执行结果
以内核对象的触发状态来调用函数
执行原理
事实上,在此功能中线程池在内部会让一个线程调用 WaitForMultipleObjects 函数,传入通过 SetThreadpoolWait 函数注冊一组句柄,并传入 false 给 bWaitAll 參数,这样当不论什么一个句柄被触发的时候,线程池就会被唤醒.因为 WaitForMultipleObjects 有一个限制,一次嘴甜仅仅能等待 64(MAXMUM_WAIT_OBJECTS)个句柄,因此线程池其实正是为每 64 个内核对象分配一个线程来进行等待,其效率还是相当高的.另外,因为 WaitForMultipleObjects 不同意我们将同一个句柄传入多次,因此我们必须确保不会用 SetThreadpoolWait 来多次注冊同一个句柄.可是,我们能够调用 DuplicateHandle, 这样就能够分别注冊原始句柄和复制句柄.
注意
绝对不要让回调函数调用 WaitForThreadpoolWait 并将自己的工作项作为參数传入,由于这样会导致死锁.另外,当线程在等待传给 SetThreadpoolWait 的句柄时,我们应该确保该句柄不会被关闭.最后我们可能并不想通过 PulseEvent 来触发一个已注冊的事件,由于当 PulseEvent 被调用的时候,我们无法保证线程池正好在等待该事件.
相关函数
- CreateThreadpoolWait (为线程池创建一个等待内核对象触发的工作对象,回调函数原型)
- SetThreadpoolWait (将内核绑定到等待内核对象触发的工作对象上)
假设想让回调函数在同一个内核对象被触发的时候再次被调用,那么须要调用 SetThreadpoolWait 来再次注冊.也能够通过 SetThreadpoolWait 来重用该等待项,即能够传入一个不同的内核对象句柄,也能够传入 NULL 来将该等待项从线程池中移除 - WaitForThreadpoolWaitCallbacks (取消已提交但未运行的等待内核对象触发的工作项 / 等待现有等待内核对象触发的工作项处理完毕把自己挂起)
- CloseThreadpoolWait (取消等待内核对象触发的工作对象)
编写步骤
代码例子
程序源代码
#include<Windows.h> #include<iostream> #include<cstdlib> using namespace std; VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WAIT Wait,TP_WAIT_RESULT WaitResult); void main() { PTP_WAIT tpWait; HANDLE hMutex; //创建等待内核对象触发的工作对象 tpWait = CreateThreadpoolWait(WaitCallback,NULL,NULL); //创建相互排斥对象 hMutex=CreateMutex(NULL,false,0); //等待相互排斥对象被释放后获得相互排斥对象拥有权 WaitForSingleObject(hMutex,INFINITE); //设置等待内核事件触发调用回调函数 SetThreadpoolWait(tpWait,hMutex,0); //释放相互排斥对象拥有权 ReleaseMutex(hMutex); //等待等待内核对象触发的工作对象结束 WaitForThreadpoolWaitCallbacks(tpWait,false); //关闭等待内核对象触发的工作对象 CloseThreadpoolWait(tpWait); system("pause"); } VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WAIT Wait,TP_WAIT_RESULT WaitResult) { cout<<"this is WaitCallback function!"<<endl; }
执行结果
以异步 I/O 请求完毕时调用函数
相关函数
- CreateThreadpoolIo (为线程池创建一个异步 I/O 请求完毕的工作对象,回调函数原型)
- StartThreadpoolIo (将嵌入在
I/O 项中的文件/设备与线程池内部的 I/O 完毕port关联起来)
在每次调用 ReadFile 和 WriteFile 之前,我们必须调用 StartThreadpoolIo.假设每次在发出 I/O 请求之前没有调用 StartThreadpoolIo,那么 IoCompletionCallback 回调函数将不会被回调 - CancelThreadpoolIo (在发出 I/O 请求之后让线程池停止调用回调函数)
- WaitForThreadpoolIoCallbacks (取消已提交但未运行的异步 I/O 请求完毕的工作项 / 等待异步 I/O 请求完毕的工作项处理完毕把自己挂起)
- CloseThreadpoolIo (取消异步 I/O 请求完毕的工作对象)
代码例子
- HANDLE hFile = CreateFileW(L"d:\test.txt", GENERIC_ALL, 0, 0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);//打开一个设备
- PTP_IO pio = CreateThreadpoolIo(hFile, OverlappedCompletionRoutine, NULL, NULL);//将设备对象和线程池的IO完毕端口关联起来。
- DWORD err = GetLastError();
- MyOverlapped ol;//使用一个自己定义的OVERLAPPED
- ol.Offset = 4;
- StartThreadpoolIo(pio);//每次发起一个异步io请求的时候。都要调一下这个函数,不然在CloseThreadpoolIo()的时候会出异常。
- BOOL rt = ReadFile(hFile, ol.GetByte(), 4, NULL, &ol);//发起一个异步IO
- MyOverlapped ol2;
- ol2.Offset = 5;
- StartThreadpoolIo(pio);
- rt = ReadFile(hFile, ol2.GetByte(), 5, NULL, &ol2);//再发起一个异步IO
- Sleep(4000);
- CloseHandle(hFile);//关闭文件对象
- CloseThreadpoolIo(pio);//关闭线程池io完毕对象
对回调函数的操作
回调函数的终止操作
线程池提供了一种便利的方法,用来描写叙述在我们的回调函数返回之后,应该运行的一些操作,回调函数用传给它的不透明的 Instance 參数(其类型为 PTP_CALLBACK_INSTANCE)来调用以下的函数
- LeaveCriticalSectionWhenCallbackReturns (当回调函数返回的时候,线程池会自己主动调用 LeaveCriticalSection,并在參数中传入指定的 CRITICAL_SECTION 结构)
- ReleaseMutexWhenCallbackReturns (当回调函数返回的时候,线程池会自己主动调用 ReleaseMutex ,并在參数传入指定的 HANDLE)
- ReleaseSemaphoreWhenCallbackReturns (当回调函数返回的时候,线程池会自己主动调用 ReleaseSemaphore,并在參数传入指定的 HANDLE)
- SetEventWhenCallbackReturns (当回调函数返回的时候,线程池会自己主动调用 SetEvent,并在參数中传入指定的 HANDLE)
- FreeLibraryWhenCallbackReturns (当回调函数返回的时候,线程池会自己主动调用 FreeLibrary, 并在參数中传入指定的 HMODULE)
另外两个函数
- CallbackMayRunLong
此函数用来通知线程池回调函数的执行时间会比較长.假设一个回调函数觉得自己须要较长的时间来处理当前的项,那么它应该调用 CallbackMayRunLong.由于线程池会坚持不创建新线程,由于长时间执行的项可能会使线程池队列中的其它项挨饿.假设 CallbackMayRunLong 返回 TRUE,那么说明线程池中还有其它线程可供使用,来对队列中的项进行处理.假设 CallbackMayRunLong 返回 false,那么说明线程池中没有其它线程能够用来处理队列中的项.为了维持线程池的执行效率,最好是让该项将它的任务划分成更小的步伐来处理(将每个部分单独地加入到线程池的队列中).任务的第一部分能够当前线程中执行. - DisassociateCurrentThreadFromCallback
回调函数调用它来告诉线程池,逻辑上自己已经完毕了工作,这使得不论什么因为调用 WaitForThreadpoolWorkCallbacks, WaitForThreadpoolTimerCallbacks, WaitForThreadpoolWaitCallbacks 或 WaitForThreadpoolIoCallbacks 而被堵塞的线程可以早一些返回,而不必等到线程池的线程从回调函数中返回