在Windows Vista中,提供了全新的线程池机制,一般这些线程池中的线程的创建的销毁是由操作系统自动完成的。
Windows Vista 中重新设计了线程池,提供了一组新的线程池API。因此,本篇讨论的仅仅在Windows Vista系统,或其以上的Windows版本中有效。
当一个进程创建之后,它并不与线程池关联。一旦新的线程池API函数被呼叫之后,系统就为该进程创建内核资源,并且有些资源直到进程结束才释放。因此,在使用线程池的时候,线程、其他内核对象、内部数据结构被分配给进程,因此要考虑线程池是否确实必要。
线程池机制有4种功能:
1、调用一个异步函数
2、定时地调用一个函数
3、当一个内核对象被通知的时候调用一个函数
4、当一个异步I/O请求完成的时候调用一个函数
而这4个功能都和线程池中的“工作项”息息相关。可以把“工作项”看作是一个特定的工作记录,记录着异步函数,线程池定时器信息,线程池等待对象信息,线程池I/O对象,而这4个对象就是实现上述4个功能的要素。
首先来讨论一下第1种功能:调用一个异步函数。其基本步骤可以有两种:
第一种:
1、定义一个给定格式的异步函数
2、提交这个异步函数给线程池
第二种:
1、定义一个给定格式的异步函数
2、创建一个“工作项”,该工作项与异步函数、异步函数参数关联
3、将这个工作项提交给线程池
4、关闭创建的“工作项”
为了在线程池中调用一个异步函数,该异步函数的定义如下(第1个参数pInstance暂不讨论,可以简单地传递NULL,下同):
然后,你可以提交一个请求给线程池,让其中的一个线程执行这个函数:
TrySubmitThreadpoolCallback 函数在线程池队列中加入一个“工作项”(work item),如果成功返回TRUE,否则返回FLASE。pcbe参数下面会介绍,你可以简单地传递NULL给这个参数(下同)。
你不必调用CreateThread来创建线程,当进程内调用TrySubmitThreadpoolCallback函数的时候,系统会自动地给你的进程创建线程池,然后在该线程池队列中队列中加入一个“工作项”,并让其中的一个线程来执行你定义的异步函数。当异步函数执行完毕后,该线程不会被销毁,而是进入线程池等待另一个“工作项”的到来。线程池中的线程是回收利用的,并不是不断创建和销毁的,这样提高了性能。同时,这个线程池如果觉得自己的线程太多的话,就自动地销毁一些线程,可以让性能达到最佳。
你可以使用CreateThreadpoolWork函数来创建一个“工作项”:
该函数接受一个异步函数的指针和这个异步函数的参数,并创建一个用户模式的数据结构来保存对应的3个参数的数据,同时返回一个指向这个数据结构的指针,可以理解为“工作项”指针。
其中,pfnWordHandler 函数是一个异步函数的指针,这个异步函数会被线程池中某个线程调用,该异步函数定义如下:
当你想将一个创建了的工作项提交给线程池,可以使用SubmitThreadpoolWork函数:
如果多次调用该函数向一个线程池提交同一个工作项,那么异步函数会被调用多次,而每次的参数都是同样一个值。
如果有另一个线程想要取消提交的工作项,或者挂起自己等待工作项完成,可以使用这个函数:
pWork 函数是一个工作项指针,由函数CreateThreadpoolWork创建并返回。如果该工作项没有被提交,则该函数马上返回,不做任何工作。
如果传递TRUE给参数bCancelPendingCallbacks,WaitForThreadpoolWorkCallbacks函数将试图取消这个先前提交的工作项。如果这个工作项正在被处理,那么这个处理不会被打断,该函数会等待直到工作项结束才返回。如果这个工作项被提交,但是目前不在处理,那么该函数就会立即取消该工作项并理解返回,那么这个工作项的异步函数就不会被调用了。
如果传递FALSE给传递bCancelPendingCallbacks,WaitForThreadpoolWorkCallbacks函数将挂起这个调用它的线程,直到指定的工作项完成,而线程池中执行这个工作项的线程在完成处理工作项之后返回线程池,继续处理下一个工作项。
如果传递给WaitForThreadpoolWorkCallbacks函数的第一个参数的工作项指针被提交给线程池多次,也就是说多个工作项使用同一个工作项指针,如果第2个参数为FALSE,那么WaitForThreadpoolWorkCallbacks将等到这个工作项指针代表的所有工作项处理完成才返回。如果传递TRUE给第2个参数,WaitForThreadpoolWorkCallbacks将等待,只要当前正在执行的工作项结束就返回。
当你不要使用工作项的时候,使用CloseThreadpoolWork函数关闭之。
这是Windows线程池提供的第2个功能。
有的时候,应用程序需要在某一个特定的时间执行特定的任务,你可以选择使用Windows内核对象“等待定时器”来实现这个功能,但是如果这种基于时间的任务特别的多,那么就不得不为每个这样的任务创建一个“等待定时器”对象,无疑会浪费资源。当然,你也许会想到创建单个“等待定时器”,然后不断地设置它的下一次要等待的时间,这样就可以完成多个基于时间的任务了。但是如此一来,代码量就会增大。
Windows提供了线程池来实现这样的功能,其方法是通过“线程池定时器”。
首先,你要定义一个如下格式的回调函数,让线程池中的线程定时调用它:
然后告诉线程池什么时候调用你的回调函数:
不难发现,CreateThreadpoolTimer函数和第1种方法中的CreateThreadpoolWork函数十分类似,而且两者的回调函数也十分类似。当调用CreateThreadpoolTimer函数的时候,第1个参数指向一个回调函数,第2个参数pvContext会传递给这个回调函数的第2个参数,而其返回值——一个“线程池定时器”指针也会传递给这个回调函数的第3个参数。
如果你想把由CreateThreadpoolTimer函数创建的“线程池定时器”注册到线程池中去,可以使用如下函数:
该函数的第1个参数pTimer是由CreateThreadpoolTimer函数返回的。第2个参数pftDueTimer是指明回调函数什么时候被调用,一个正的数值表示的是绝对时间,即UTC统一时间;一个负数表示相对时间,即调用该函数之后开始计时,以毫秒为单位;如果是-1,表明回调函数马上被调用。第3个参数msPeriod表明周期性地调用回调函数的时间间隔,即周期时间,如果只想回调函数调用一次,传递0给这个参数。第4个参数是和第3个参数联用的,表明周期时间的波动范围,比如,msPeriod=1000,msWindowLength=2,那么回调函数会在每隔998、999、1000、1001、1002这5个可能的毫秒时间被调用。
如果一个“线程池定时器”已经被SetThreadpoolTimer设置了,那么可以再次呼叫SetThreadpoolTimer函数来更改它的相关属性。呼叫SetThreadpoolTimer的时候,可以把NULL传递给第2个参数pftDueTime,这样就说明让线程池停止呼叫对应的回调函数。
你可以查询一个“线程池定时器”是否被设置,呼叫IsThreadpoolTimerSet函数:
你也可以让线程等待一个“线程池定时器”完成工作,呼叫函数WaitForThreadpoolTimerCallbacks,当要关闭一个“线程池定时器”的时候,呼叫函数CloseThreadpoolTimer,这两个函数同前面讨论的WaitForThreadpoolWork和CloseThreadpoolWorkCallbacks函数类似,可以参考本篇前面的内容。
下面总结一下“线程池定时器”的使用方法:
有很多线程,初始化的时候等待一个内核对象,一旦这个内核对象转入“已通知”状态,线程就会通知另外一些线程,然后转回继续等待这个内核对象。但是,如果这样的线程很多的话,无疑会增大系统的开销。
此时,你可以考虑使用线程池来实现这个功能,就是当一个内核对象被通知的时候,由线程池中的一个线程调用一个异步的回调函数。
如果你想让一个“工作项”在一个内核对象为“已通知”的状态下被执行,这个基本流程和前面两个功能的流程类似。
首先,定义一个如下格式的异步函数:
然后,需要创建一个“线程池等待对象”:
接着就可以将创建的“线程池等待对象”与这个线程池关联起来,此时线程池队列中会有一个“等待项”记录:
这个函数的第1个参数pWaitItem很显然是从CreateThreadpoolWait成功返回的“线程池等待对象”指针。第2个参数hObject是一个内核对象句柄,当这个内核对象为“已通知”状态,则线程池中的一个线程调用异步回调函数。第3个参数pftTimeout是一个等待内核对象的时间,如果为0表示不等待;传递一个负数表示一个相对时间;传递一个正数表示绝对时间;传递NULL表示无限期地等待。
要注意的是,不要多次使用SetThreadpoolWait来等待同一个hObject。
当内核对象被通知或者等待时间超出,线程池中的线程将呼叫你的回调函数,这个回调函数的最后一个参数WaitResult的值,其实是一个DOWRD类型的,它指明的该回调函数被调用的原因:
1、WAIT_OBJECT_0:SetThreadpoolWait中第二个参数hObject所表明的内核对象受到通知。
2、WAIT_TIMEOUT:内核对象受到通知的时间超过了SetThreadpoolWait的第三个参数所设置的等待时间。
3、WAIT_ABANDONED_0:SetThreadWait函数第二个参数hObject代表一个互斥内核对象,而这个互斥内核对象被丢弃。
一旦一个线程池线程调用了你的回调函数,那么对应的“等待项”就不活跃了,你必须使用相同的参数再次调用SetThreadpoolWait函数来提交一个等待项。
如果想删除一个“等待项”,可以使用与之对应的“线程池等待对象”指针来调用SetThreadpoolWait,并将hObejct参数设置为NULL。
最后,你也可以使用WaitForThreadpoolWaitCallbacks来等待对应的“等待项”结束,也可以使用CloseThreadpoolWait来关闭一个“等待项”。这两个参数和WaitForThreadpoolWorkCallbakcs和CloseThreadpoolWork是类似的。
读过上面3中线程池的功能,不难发现有很多共同的特点,连函数名称都很有规律。线程池中的线程由系统统一管理,自动地创建和销毁。其实,这些线程内部都在等待一个I/O完成端口,这个I/O完成端口称为“线程池的I/O完成端口”。
如果你要使用线程池来处理设备异步I/O请求的时候,当你打开一个设备的时候,必须首先将这个设备与“线程池I/O完成端口”关联起来,然后告诉线程池当设备异步I/O请求结束之后哪个函数将被调用。
首先,定义一个如下格式的异步回调函数:
这个函数的最后一个参数pIo是一个PTP_IO类型,即一个线程池I/O完成项,它与“线程池工作项”和“线程池等待项”是类似的。你必须创建它,使用如下函数:
该函数将hDevice参数所对应的设备记录到线程池I/O项中,然后,可以使用如下函数将设备与线程池I/O完成端口关联起来:
注意,StartThreadpoolIo函数必须在ReadFile和WriteFile之前调用,如果没有在它们之前调用,你的异步回调函数不会被调用。
当你想停止调用回调函数的时候,可以使用CancelThreadpoolIo,如果在调用ReadFile或WriteFile之后,它们的返回值是FLASE,而GetLastError的返回值不是ERROR_IO_PENDING,那么也应该调用CancelThreadpoolIo:
当结束了设备I/O,你应该使用CloseHandle关闭设备句柄,然后呼叫CloseThradpoolIo关闭线程池I/O项,即取消设备与线程池I/O请求的关联。
另外,你可以让一个线程等待I/O请求结束:
如果给这个函数的参数BCancelPendingCallbacks传递TRUE,那么回调函数将不会被调用,该函数的用法和WaitForThreadpoolWork是类似的。
注意上面讨论的各种类型的回调函数第1个参数,是一个PTF_CALLBACK_INSTANCE类型的数据pInstance,从字面上看,是“线程池回调函数实体指针”,也就是说,这个数据是各个回调函数唯一的,是回调函数的标识,这个数据是在调用回调函数之前由系统自动分配的,可以用这个参数调用如下函数:
这些函数的第1个参数pci标识当线程池前正在处理的工作、定时器、等待、I/O项,调用这些函数,表示对应的回调函数结束之后,所做的一些释放和设置工作。
其中,前4个函数,提供了一种方法来通知其他线程,说明线程池中的某一个工作项完成。最后一个函数,提供了一种方法来卸载DLL的方法,特别是当回调函数是从DLL中导出的时候,这种方法特别适用。要注意的是,只能有一个动作在回调函数返回的时候被执行,你不能多次呼叫上述5个函数,这样的话,最后一次呼叫的函数会覆盖前面所呼叫的函数,因此,不能同时离开关键代码段并释放信号量内核对象。
另外,还有两个函数需要回调函数实体指针:
CallbackMayRunLong并不是设置回调函数结束时的工作的,而是当一个回调函数认为自己执行的时间可能比较长才可能需要呼叫这个函数。此时,线程池不会创建新的线程,以此来提高这个回调函数的性能。当该函数返回FLASE,线程池不允许其他线程能够处理线程池队列中的其他项;如果返回TRUE,表示线程池允许其他线程处理其他工作项。
DisassociateCurrentThreadFromCallback函数表明与回调函数关联的工作项在“逻辑上”完成了(其实不是真正完成),此时允许等待在这个工作项上的函数返回,比如WaitForThreadpoolWorkCallbacks、WaitForThreadpoolTimerCallbacks、WaitForThreadpoolWaitCallbacks、WaitForThreadpoolIoCallbacks这些函数返回。
以上所讨论的线程池,都是系统自动控制的,用户无法改变其内部的流程。
下面,我们讨论一下如何自己定制线程池。
你会注意到,上面的每个“创建”函数:CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait、CreateThreadpoolIo以及TrySubmitThreadpoolCallback这5个函数中的最后一个参数pcbe,一个类型为PTP_CALLBACK_ENVIRON的参数,一个指向“回调函数环境”结构的指针。你可以简单地传递NULL给这个参数,表明你使用默认的系统自动分配和管理的进程线程池。
但是,有的时候程序员喜欢自己来控制线城池,给线城池设置一些规则和属性。比如设置改线城池中线程的数量上下限,或者想操纵线城池中线程的创建和销毁。
要达到这个目的,可以自己创建线程池,然后设置一些属性。
首先,创建一个线程池,使CreateThreadpool函数:
该函数返回一个PTP_POOL类型的数据,姑且认为是“线城池指针”的意思,即代表了一个线程池。
然后,就可以通过这个线城池指针来呼叫相应的API函数,设置线程池的一些属性了。
你可以设置线城池的线程数量的上下限:
BOOL SetThreadpoolThreadMinimum(
PTP_POOL pThreadPool,
DWORD cthrdMin);
BOOL SetThreadpoolThreadMaximum(
PTP_POOL pThreadPool,
DWORD cthrdMost);
通过呼叫这两个函数之后,线程池中的线程的数量决不会少于设置的最小值,并且允许这个数量增大到最大值。顺便说一下,默认的线城池的线程数量范围为1~500。
当一个线程池需要关闭的时候,呼叫CloseThreadpool函数:
呼叫这个函数之后,对应的线程池队列中的项都不会被处理,当前正在处理工作项的线程都会结束处理然后线程终止,其他还没有被处理的项都会被取消。
一旦你创建了你自己的线程池并设置了线程数量上下限,你就初始化那个“回调函数环境”结构了,该结构中包含了另外的一些设置。这个结构与一个工作项有关。该结构定义如下:
你最好不要自己去初始化该结构,而是应该使用如下函数:
该函数将结构中的各个字段设置为0,除了Version被设置为1。
当你不再需要使用该结构的时候,使用如下函数删除它:
在初始化TP_CALLBACK_ENVIRON结构之后,该结构与一个工作项相关,然后你就可以使用这个结构来提交一个工作项给指定的线程池了:
如果不调用该函数,那么回调函数环境结构中的Pool成员的值就是NULL,表示不为任何定制的线程池相关,那么与此结构相关的工作项就会提交给默认线程池。
如果一个工作项处理的时间比较长,可以调用SetThreadpoolCallbackRunsLong函数,这会导致线程池更快地创建线程来处理这个工作项。
你可以呼叫SetThreadpoolCallbackLibrary来确保当某个工作项未完成的时候,一个DLL始终被加载到进程地址中。该函数也可以除去潜在的死锁。
线程池要处理很多的项目,因此很难确定这些项目什么时候处理结束,这也使得线程池的清除工作困难了。为了解决这种情况,线程池提供了一种机制——“清理组”。要注意的是,该机制不适合于默认线程池,只适合于定制的线程池。因为默认线程池的生命期与进程一样,系统会在进程结束之后清除默认线程池。
为了使用“清理组”机制,首先,你需要创建一个清理组:
然后将已创建的清理组与线程池关联起来:
该函数在内部将回调函数指针p的cbe参数CleanupGroup和CleanupCancelCallback成员设置为第2和第3个参数所提供的值。如果清理组被取消,第3个参数pfng表示的回调函数将会被调用,这个回调函数必须满足如下格式:
每次你调用CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait和CreateThreadpoolIo的时候,如果传递给它们的最后一个参数pcbe不是NULL,那么一项就会加入相关的组清理中,当这样的项都完成之后,你调用CloseThreadpoolWork、CloseThreadpoolTimer、CloseThreadpoolWait、ClsoeThreadpoolIo的时候,又暗中地将这样的项从组清理中删除了。
这个时候,想要删除线程池,可以调用如下函数:
这个函数同以前的WaitForThreadpool*函数类似,当一个线程呼叫它时,它等待,直到所有的在线程工作组的项完成处理。如果第2个参数为TRUE,会取消所有还没有开始执行的项目,然后等待当前正在执行的项目,直到它们执行完成后该函数返回。如果第2个参数为TRUE,而且SetThreadpoolCallbackCleanupGroup的最后一个参数pfng传递了一个回调函数指针,那么这个回调函数就会为每个被取消的项目调用一次,CloseThreadpoolCleanupGroupMembers函数的第3个参数pvCleanupContext就会传入回调函数的第2个参数pvCleanupContext。
如果在呼叫CloseThreadpoolCleanupGroupMembers函数的时候,传递FLASE给第2个参数,那么该函数就会等待线程池中队列中的所有的项完成之后才返回,此时回调函数不会被调用,因此可以传递NULL给pvCleanupContext参数。
当ClsetThreadpoolCleanupGroupMembers函数返回之后,你需要呼叫CloseThreadpoolCleanupGroup来关闭一个线程池清理组:
最后,要调用DestroyThreadpoolEvironment和CloseThreadpool函数来清除回调函数环境结构和关闭线程池。
通过前面的叙述,线程池的操作流程是比较固定的:
1、定义异步回调函数
2、创建相关项
3、提交或设置项
4、线程池执行
5、关闭相关项
自己写了一段代码,总结了前面的知识,代码中省略了第1步,即没有定义回调函数,因为这是根据需要而编写的。如果代码中有错误,还请大家指出。