线程池的目的就是为了减少创建和销毁线程的额外开销,利用已经存在的线程多次循环执行多个任务从而提高系统的处理能力.
线程池会自动地根据内制的算法增加或减少线程池中的线程或为程序增加新的线程池。
1.异步方法调用
异步方法调用有以下两种方法:
(1)线程函数原型(回调函数)
VOID NTAPI SimpleCallback
(
PTP_CALLBACK_INSTANCE pInstance, // See "Callback Termination Actions" section
PVOID pvContext
);
TrySubmitThreadpoolCallback
该函数将线程函数执行请求发到线程池,并将一个"工作项目"添加到线程池的队列中。
注:我们不需要调用CreateThread函数,线程池中的线程会执行我们的回调函数
(2)显示控制"工作项目"
CreateThreadpoolWork 创建一个工作项目
SubmitThreadpoolWork 将工作项目提交到线程池中,一个工作项目可以多次提交到线程池中。
WaitForThreadpoolWorkCallbacks 等待线程函数执行完毕或取消执行线程函数。
CreateThreadpoolWork要求的线程函数原型:
VOID CALLBACK WorkCallback
(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_WORK Work
);
VOID WaitForThreadpoolWorkCallbacks
(
PTP_WORK pWork,
BOOL bCancelPendingCallbacks
);
2.时间间隔内调用函数
(1)CreateThreadpoolTimer要求的线程函数原型
VOID CALLBACK TimeoutCallback
(
PTP_CALLBACK_INSTANCE pInstance, // See "Callback Termination Actions" section
PVOID pvContext,
PTP_TIMER pTimer
);
(2)步骤
CreateThreadpoolTimer
SetThreadpoolTimer
WaitForThreadpoolTimerCallbacks
CloseThreadpoolTimer
3.当内核对象处于Signal状态时调用函数
当指定的内核对象变成Signal状态或等待超时,线程池会执行用户指定的线程函数。
之后当内核对象再次变成Signal状态时,线程函数不会被调用,除非再次调用SetThreadpoolWait注册线程函数。
(1)CreateThreadpoolWait要求的线程函数原型
VOID CALLBACK WaitCallback
(
PTP_CALLBACK_INSTANCE pInstance, // See "Callback Termination Actions" section
PVOID Context,
PTP_WAIT Wait,
TP_WAIT_RESULT WaitResult
);
CreateThreadpoolWait
SetThreadpoolWait 不允许多次注册同样的Handle,但是我们可以用DuplicateHandle函数复制一个句柄 然后再注册。
WaitForThreadpoolWaitCallbacks
CloseThreadpoolWait
4.当异步I/O请求结束后调用函数
To be filled
5.回调终结后的操作和私有线程
To be filled
6. Common API
TrySubmitThreadpoolCallback
CreateThreadpoolWork SubmitThreadpoolWork WaitForThreadpoolWorkCallbacks CloseThreadpoolWork
CreateThreadpoolTimer SetThreadpoolTimer WaitForThreadpoolTimerCallbacks CloseThreadpoolTimer
CreateThreadpoolWait SetThreadpoolWait WaitForThreadpoolWaitCallbacks CloseThreadpoolWait
CallbackMayRunLong DisassociateCurrentThreadFromCallback
1. 链接选项“/statck:reserve[,commit]”可以在PE文件中记录默认的线程栈保留大小和提交大小,实际栈大小还要结合_beginthreadex时的参数。
2. PAGE_GUARD属性的作用:第一次访问具有该属性的页面,会触发一个STATUS_GUARD_PAGE_VIOLATION异常,同时该属性被自动抹除,于是后续的访问正常。即该属性用于首次访问的通知。
3. 默认条件下,线程栈创建时先reserve一块1MB的内存,栈底的两块页面被提交,其中较低地址的那块页面具有PAGE_GUARD属性,被称为保护页面(Guard Page)。当栈的调用层次变深需要更多内存时,系统去掉当前保护页面的PAGE_GUARD属性并提交下一个页面作为保护页面(实现方式见条款4)。这个过程进行下去,栈顶所在的提交页面之后始终有一块被提交的保护页面,直到栈的调用层次足够深,当倒数第二个页面被提交并需要标记为保护页面的时候,这个标记行为终止并抛出EXCEPTION_STACK_OVERFLOW异常。栈最低地址的一个页面始终处于reserve状态,用来隔离栈和栈下方的内存空间,避免非法的栈操作访问越界。捕获了栈溢出结构化异常的线程由于没有了保护页面,需要调用_resetstkoflw来重新标记保护页,否则下次调用层次太深的时候会因为没有保护页不触发栈溢出异常直接访问到最低地址的reserve页,造成非法访问错误。
4. 栈上reserve页被从高到底依次commit的方式:当位于栈顶的函数帧在保护页面中时,访问保护页内存会触发异常,系统捕获异常,提交下一页,并判断下一页是否是倒数第二页,是的话抛出栈溢出异常,否则将一下页标记为保护页。如果栈顶函数帧很大(比如包含大数组),跨越多个分页,由于函数内部可能先访问函数帧中最低地址的reserve页的内存,引起非法访问错误,于是C++编译器对这种栈帧大于1个分页的函数进行了特殊处理:编译器会在大栈帧函数的开始插入_chkstk,后者会沿大栈帧的底部向顶部依次访问每个分页,连续推动保护页,保证后来函数体中的随机访问都作用在commit分页上。
5. Debug版本程序在调用函数前,会备份当前栈的上下文,在函数返回后对比新的栈数据和备份数据,判断是否有栈上的越界错误。Release版本程序开启/Gs开关后能起到类似的效果。
以异步方式调用函数
为了用线程池来以异步的方式执行一个函数,我们需要定义一个具有以下原型的函数:
VOID CALLBACK SimpleCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context
);
然后为了让线程池中的一个线程来执行该函数,我们需要向线程池提交一个请求:
BOOL WINAPI TrySubmitThreadpoolCallback(
__in PTP_SIMPLE_CALLBACK pfns,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
注意,我们从来不用自己调用CreateThread.系统会自动为我们的进城创建一个默认的线程池,并让线程池中的一个线程来调用我们的回调函数.此外,当这个线程处理完一个客户请求后,不会立即销毁,而是回到线程池,准备好处理队列中的任何其他工作项.
每一次调用TrySubmitThreadpoolCallback函数的时候,系统会在内部以我们的名义分配一个工作项。但是在某些情况下,比如内存不足,或者配额限制,TrySubmitThreadpoolCallback调用可能会失败。在多项操作需要相互协调的时候(比如一个计时器要依靠一个工作项来取消一个操作的时候),这是不能接受的。
所以,我们必须在设置计时器的时候手动创建一个工作项:
PTP_WORK WINAPI CreateThreadpoolWork(
__in PTP_WORK_CALLBACK pfnwk,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
参数pfnwk 必须符合下面的函数原型:
VOID CALLBACK WorkCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context,
[in, out] PTP_WORK Work
);
当我们要向线程池提交一个工作项的时候,可以调用如下函数:
VOID WINAPI SubmitThreadpoolWork(
__in_out PTP_WORK pwk
);
如果我们有另外一个线程,该线程想要取消已经提交的工作项,或者该线程由于要等待工作项处理完毕而要将自己挂起:
VOID WINAPI WaitForThreadpoolWorkCallbacks(
__in_out PTP_WORK pwk,
__in BOOL fCancelPendingCallbacks
);
如果参数fCancelPendingCallbacks 为TRUE,那么WaitForThreadpoolWorkCallbacks 会试图取消先前提交的那个工作项。如果线程池中的线程正在处理那个工作项,那么WaitForThreadpoolWorkCallbacks 函数会一直等到该工作项已经完成后再返回。如果已提交的工作项尚未被任何线程处理,那么函数会先将它标记为已取消,然后立即返回。当完成端口从队列中取出该工作项的时候,线程池知道无需调用回调函数,这样该工作项就不会被执行。
如果参数fCancelPendingCallbacks 为FALSE,那么WaitForThreadpoolWorkCallbacks 会将调用线程挂起,直到指定工作项处理完成,而且线程池中处理该工作项的线程也已经被收回并准备好处理下一个工作项为止。
说明:
1.如果打算提交大量的工作项,那么出于对性能和内存使用的考虑,创建工作项一次,然后多次提交它会更好。
2.如果用一个PTR_WORK对象提交了多个工作项,而且传给WaitForThreadpoolWorkCallbacks 函数中的fCancelPendingCallbacks 为FALSE,那么WaitForThreadpoolWorkCallbacks 函数会等待线程池处理完所有已提交的工作。如果传给fCancelPendingCallbacks 为TRUE,那么只会等待当前正在运行的工作项完成。
如果不在需要一个工作项,可以调用如下函数:
VOID WINAPI CloseThreadpoolWait(
__in_out PTP_WAIT pwa
);
每隔一段时间调用一个函数
为了将一个工作项安排在某个时间执行,我们必须定义一个回调函数,原型如下:
VOID CALLBACK TimeoutCallback(
TP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_TIMER pTimer);
然后调用下面的函数来通知线程池应该在何时调用我们的函数:
PTP_TIMER WINAPI CreateThreadpoolTimer(
__in PTP_TIMER_CALLBACK pfnti,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
当我们想要向线程池注册计时器的时候,应该调用如下函数:
VOID WINAPI SetThreadpoolTimer(
__in_out PTP_TIMER pti,
__in_opt PFILETIME pftDueTime,
__in DWORD msPeriod,
__in_opt DWORD msWindowLength
);
参数pftDueTime 表示第一次调用回调函数应该是在什么时候。我们可以传一个负值(以微妙为单位)来指定一个相对时间,该时间相对于调用SetThreadpoolTimer 的时间。
-1表示立即开始。
为了指定一个绝对时间,我们应该传入一个正值,这个值以100纳秒为单位。
如果只想让计时器触发一次,那么可以给msPeriod 参数传0。
如果想让线程池定期调用我们的回调函数,那么应该给msPeriod 参数传一个正值(表示在再次调用我们的TimerCallback之前需要等待多少毫秒。)
参数msWindowLength 用来给回调函数的执行时间增加一些随机性,这使得回调函数会在当前设定的促发时间,到当前设定的促发时间加上msWindowLength设定的时间之间触发。
在设置了计时器之后,我们还可以调用SetThreadpoolTimer函数并在pti 参数中传入先前设置的计时器指针,以此来对已有的计时器进行修改。
我们还可以给pftDueTime 传递NULL,这等于是告诉线程池停止调用我们的TimerCallback函数。这不失为一种将计时器暂停但又不必销毁计时器对象的好方法。
我们可以调用如下函数来确定某个计时器是否已经被设置,既它的pftDueTime 参数值不为NULL。
BOOL WINAPI IsThreadpoolTimerSet(
__in_out PTP_TIMER pti
);
最后,我们还可以调用如下函数:
VOID WINAPI WaitForThreadpoolTimerCallbacks(
__in_out PTP_TIMER pti,
__in BOOL fCancelPendingCallbacks
);
VOID WINAPI CloseThreadpoolTimer(
__in_out PTP_TIMER pti
);
在内核对象触发时调用一个函数
如果要注册一个工作项,让它在一个内核对象被触发的时候执行,那么首先要创建一个回调函数:
VOID CALLBACK WaitCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context,
[in, out] PTP_WAIT Wait,
[in] TP_WAIT_RESULT WaitResult
);
然后创建线程池对象:
PTP_WAIT WINAPI CreateThreadpoolWait(
__in PTP_WAIT_CALLBACK pfnwa,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
将一个内核对象绑定到这个线程池:
VOID WINAPI SetThreadpoolWait(
__in_out PTP_WAIT pwa,
__in_opt HANDLE h,
__in_opt PFILETIME pftTimeout
);
pwa 参数用来标识CreateThreadpoolWait返回的对象。
h 参数用来标识某个内核对象。
pftTimeout 参数用来表示线程池最长应该花多少时间来等待内核对象被触发。传0表示不用等待,传负值表示相对时间,传正值表示绝对时间,传NULL表示无限长等待。
当内核对象被触发或者超出等待时间的时候,线程池中的某个线程会调用我们的WaitCallback函数。
注意:
一旦线程池的一个线程调用了我们的回调函数,对应等待项(wait item)将进入不活跃状态。“不活跃“意味着如果我们想让回调函数再次被调用,那么我们就必须调用SetThreadpoolWait 函数来再次注册一个内核对象。
在异步I/O请求完成时调用一个函数
首先,必须编写符合以下原型的函数:
VOID CALLBACK OverlappedCompletionRoultine (
PTP_CALLBACK_INSTANCE pInstance,
PVOID pvContext,
PVOID POverlapped,
ULONG IoResult,
ULONG_PTR NumberOfBytesTransferred,
PTP_IO pIo);
当一个I/O操作完成时,这个函数会被调用并得到一个指向OVERLAPPED结构的指针,这个指针式我们在调用ReadFile或WriteFile来发出I/O请求的时候(通过pOverlapped参数)传入的。
操作结果通过IoResult参数传入,如果I/O成功,那么该参数为NO_ERROR。
已传输的字节数通过NumberOfByteTransferred参数传入。
pIo则是一个指向线程池中I/O项的指针。
创建线程池I/O对象:
PTP_IO WINAPI CreateThreadpoolIo(
__in HANDLE fl,
__in PTP_WIN32_IO_CALLBACK pfnio,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
参数fl 为我们想要与线程池内部的I/O完成端口相关联的文件/设备句柄(通过用FILE_FLAG_OVERLAPPED标志调用CreateFile函数所打开的)。
当线程池I/O对象创建完毕后,我们通过调用下面的函数来将嵌入在I/O项中的文件/设备与线程池内部I/O完成端口相关联:
VOID WINAPI StartThreadpoolIo(
__in_out PTP_IO pio
);
注意:
在调用ReadFile和 WriteFile之前,我们必须调用StartThreadpoolIo。如果每次在发出I/O请求之前没有调用StartThreadpoolIo,那么我们的OverlappedCompletionRoultine回调函数将不会被调用。
如果在发出I/O请求之后让线程池停止调用我们的回调函数:
VOID WINAPI CancelThreadpoolIo(
__in_out PTP_IO pio
);
如果在发出请求的时候,ReadFile 和 WriteFile 调用失败了,那么我们仍然必须调用CancelThreadpoolIo。例如,如果这两个函数的返回值为FALSE 并且 GetLastError的返回值为ERROR_IO_PENDING以外的值时。
当对文件/设备的使用完成后,我们应该调用CloseHandle来将其关闭,并调用如下函数来解除它与线程池的关联:
VOID WINAPI CloseThreadpoolIo(
__in_out PTP_IO pio
);
我们还可以调用下面的函数来让另一个线程等待一个待处理的I/O请求完成:
VOID WINAPI WaitForThreadpoolIoCallbacks(
__in_out PTP_IO pio,
__in BOOL fCancelPendingCallbacks
);
如果传给参数fCancelPendingCallbacks 的值为TRUE,那么当请求完成的时候,我们的回调函数不会被调用。这和调用CancelThreadpoolIo 函数的功能相似。
对线程池进行定制
在调用 CreateThreadpoolWork ,CreateThreadpoolIo 等函数的时候,我们给参数PTP_CALLBACK_ENVIRON 设为NULL,那么我们会将工作项添加到进程默认的线程池中,默认的线程池的配置能够很好的满足大多应用程序的要求。
但是,有时我们想修改线程池中可运行线程的最小数量和最大数量,这时,我们可以调用下面的函数来创建一个新的线程池:
PTP_POOL WINAPI CreateThreadpool(
PVOID reserved
);
参数reserved 是保留的,因此我们应该传NULL.
设置线程池中线程的最大数量和最小数量:
VOID WINAPI SetThreadpoolThreadMaximum(
__in_out PTP_POOL ptpp,
__in DWORD cthrdMost
);
BOOL WINAPI SetThreadpoolThreadMinimum(
__in_out PTP_POOL ptpp,
__in DWORD cthrdMic
);
默认线程池的最小数量是1,最大数量为500。
当应用程序不再需要自己定制的线程池时:
VOID WINAPI CloseThreadpool(
__in_out PTP_POOL ptpp
);
线程池中当前正在处理队列中的项的线程会完成他们的处理并终止。而线程池的队列中所有尚未开始处理的项将被取消。
一旦我们创建了自己的线程池,并指定了线程的最小数量和最大数量,我们就可以初始化一个回调环境(callback environment),既前面函数中传入的PTP_CALLBACK_ENVIRON类型参数指向的数据结构。
线程池回调环境的数据结构在winnt.h中定义如下:
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;
对上面结构进行初始化:
VOID InitializeThreadpoolEnvironment(
__out PTP_CALLBACK_ENVIRON pcbe
);
在我们不需要线程池的回调环境时:
VOID DestroyThreadpoolEnvironment(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
为了将一个工作项添加到线程池的队列中,回调环境必须标明该工作项必须由哪个线程池来处理:
VOID SetThreadpoolCallbackPool(
__in_out PTP_CALLBACK_ENVIRON pcbe,
__in PTP_POOL ptpp
);
如果我们不调用上面的函数,那么TP_CALLBACK_ENVIRON 的Pool字段会一直为NULL,当用这个回调环境来添加工作项的时候,工作项会被添加进默认的线程池。
调用如下函数来告诉回调环境,工作项通常需要较长的时间来处理,这样会使得线程池会更快的创建线程:
VOID SetThreadpoolCallbackRunsLong(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
得体的销毁线程池:清理组
默认的线程池的生命周期与进程相同,在进程终止的时候,Windows会将其销毁并负责所有清理工作。
我们要清理的是我们创建的私有的线程池。
首先创建一个清理组:
PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(void);
然后将这个清理组与一个已经绑定到线程池的TP_CALLBACK_ENVIRON 结构关联起来
VOID SetThreadpoolCallbackCleanupGroup(
__in_out PTP_CALLBACK_ENVIRON pcbe,
__in PTP_CLEANUP_GROUP ptpcg,
__in_opt PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);
如果传给pfng 参数的值不为NULL,那么回调函数必须符合下面的原型:
VOID CALLBACK CleanupGroupCancelCallback(
[in, out, optional] PVOID ObjectContext,
[in, out, optional] PVOID CleanupContext
);
销毁线程池:
VOID WINAPI CloseThreadpoolCleanupGroupMembers(
__in_out PTP_CLEANUP_GROUP ptpcg,
__in BOOL fCancelPendingCallbacks,
__in_out_opt PVOID pvCleanupContext
);
1.如果fCancelPendingCallbacks 参数为TRUE,并且传给SetThreadpoolCallbackCleanupGroup函数的参数pfng值是一个CleanupGroupCancelCallback 函数的地址,那么对于每一个被取消的工作项,我们的回调函数都会被调用。
2.如果fCancelPendingCallbacks 参数为TRUE,那么所有已提交但尚未处理的工作项直接取消,函数会在所有当前正在运行的工作项完成之后返回。.
参数pvCleanupContext 会返回包含每个被取消的项的上下文。用于在调用CleanupGroupCancelCallback中传入CleanupContext 的值。
3.如果fCancelPendingCallbacks 参数为FALSE,那么在返回之前,线程池会花时间来处理队列中所有剩余的项,由于项会全部处理完,因此可以给pvCleanupContext参数传NULL。
.
当所有的工作项被取消或者被处理之后,我们调用如下函数来释放清理组所占用的资源:
VOID WINAPI CloseThreadpoolCleanupGroup(
__in_out PTP_CLEANUP_GROUP ptpcg
);
最后调用:
VOID DestroyThreadpoolEnvironment(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
VOID WINAPI CloseThreadpool(
__in_out PTP_POOL ptpp
);