Windows核心编程011.pdf
《Windows核心编程011.pdf》由会员分享,可在线阅读,更多相关《Windows核心编程011.pdf(13页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、下载第11章线程池的使用第8章讲述了如何使用让线程保持用户方式的机制来实现线程同步的方法。用户方式的同步机制的出色之处在于它的同步速度很快。如果关心线程的运行速度,那么应该了解一下用户方式的同步机制是否适用。到目前为止,已经知道创建多线程应用程序是非常困难的。需要会面临两个大问题。一个是要对线程的创建和撤消进行管理,另一个是要对线程对资源的访问实施同步。为了对资源访问实施同步,Wi n d o w s提供了许多基本要素来帮助进行操作,如事件、信标、互斥对象和关键代码段等。这些基本要素的使用都非常方便。为了使操作变得更加方便,唯一的方法是让系统能够自动保护共享资源。不幸的是,在 Wi n d o
2、 w s提供一种让人满意的保护方法之前,我们已经有了一种这样的方法。在如何对线程的创建和撤消进行管理的问题上,人人都有自己的好主意。近年来,我自己创建了若干不同的线程池实现代码,每个实现代码都进行了很好的调整,以便适应特定环境的需要。M i c r o s o f t公司的Windows 2000提供了一些新的线程池函数,使得线程的创建、撤消和基本管理变得更加容易。这个新的通用线程池并不完全适合每一种环境,但是它常常可以适合你的需要,并且能够节省大量的程序开发时间。新的线程池函数使你能够执行下列操作:异步调用函数。按照规定的时间间隔调用函数。当单个内核对象变为已通知状态时调用函数。当异步I/O
3、请求完成时调用函数。为了完成这些操作,线程池由 4个独立的部分组成。表 11-1显示了这些组件并描述了控制其行为特性的规则。表11-1 线程池的组件及其行为特性组件定时器等待I/O非I/O线程的初始数值总是1000当创建一个线程时当调用第一个线程每6 3个注册对象有系统使用试探法,但是这里有一些因池函数时一个线程素会影响线程的创建:自从添加线程后已经过去一定的时间(以秒计算)使用WT_EXECUTELONGFUNCTION 标志 已经排队的工作项目的数量超过了某个阈值当线程被撤消时当进程终止运行时当已经注册的等待当线程没有未处当线程空闲了对象数量是0时理的I/O请求并且已一个阈值周期(约经空闲
4、了一个阈值1 m s)时周期(约1 m s)时线程如何等待待命状态Wa i t F o r M u l t i p l e O b-待命状态G e t Q u e u e d-C o m p l ej e c t st i o n-S t a t u s(续)组件定时器等待I/O非I/O是什么唤醒了线程等待定时器通知排内核对象变为已通知排队的用户A P C和展示已完成的队的用户A P C状态已完成的I/O请求状态和I/O请示(完成端口最多允许数量为2*的C P U线程同时运行的数量)当进程初始化时,它并不产生与这些组件相关联的任何开销。但是,一旦新线程池函数之一被调用时,就为进程创建某些组件,
5、并且其中有些组件将被保留,直到进程终止运行为止。如你所见,使用线程池所产生的开销并不小。相当多的线程和内部数据结构变成了你的进程的一个组成部分。因此必须认真考虑线程池能够为你做什么和不能做什么,不要盲目地使用这些函数。好了,上述说明已经足够了。下面让我们来看一看这些函数能够做些什么。11.1 方案1:异步调用函数假设有一个服务器进程,该进程有一个主线程,正在等待客户机的请求。当主线程收到该请求时,它就产生一个专门的线程,以便处理该请求。这使得应用程序的主线程循环运行,并等待另一个客户机的请求。这个方案是客户机/服务器应用程序的典型实现方法。虽然它的实现方法非常明确,但是也可以使用新线程池函数来
6、实现它。当服务器进程的主线程收到客户机的请求时,它可以调用下面这个函数:该函数将一个“工作项目”排队放入线程池中的一个线程中并且立即返回。所谓工作项目是指一个(用 p f n C a l l b a c k参数标识的)函数,它被调用并传递单个参数 p v C o n t e x t。最后,线程池中的某个线程将处理该工作项目,导致函数被调用。所编的回调函数必须采用下面的原型:尽管必须使这个函数的原型返回D W O R D,但是它的返回值实际上被忽略了。注意,你自己从来不调用C r e a t e T h r e a d。系统会自动为你的进程创建一个线程池,线程池中的一个线程将调用你的函数。另外,
7、当该线程处理完客户机的请求之后,该线程并不立即被撤消。它要返回线程池,这样它就可以准备处理已经排队的任何其他工作项目。你的应用程序的运行效率可能会变得更高,因为不必为每个客户机请求创建和撤消线程。另外,由于线程与完成端口相关联,因此可以同时运行的线程数量限制为 C P U数量的两倍。这就减少了线程的上下文转移的开销。该函数的内部运行情况是,Q u e u e U s e r Wo r k I t e m检查非I/O组件中的线程数量,然后根据负荷量(已排队的工作项目的数量)将另一个线程添加给该组件。接着 Q u e u e U s e r Wo r k I t e m执行对P o s t Q u
8、 e u e d C o m p l e t i o n S t a t u s的等价调用,将工作项目的信息传递给 I/O完成端口。最后,在完成端口上等待的线程取出信息(通过调用 G e t Q u e u e d C o m p l e t i o n S t a t u s),并调用函数。当函第 11章线程池的使用计计275下载数返回时,该线程再次调用G e t Q u e u e d C o m p l e t i o n S t a t u s,以便等待另一个工作项目。线程池希望经常处理异步 I/O请求,即每当线程将一个 I/O请求排队放入设备驱动程序时,便要处理异步I/O请求。当设备
9、驱动程序执行该I/O时,请求排队的线程并没有中断运行,而是继续执行其他指令。异步 I/O是创建高性能可伸缩的应用程序的秘诀,因为它允许单个线程处理来自不同客户机的请求。该线程不必顺序处理这些请求,也不必在等待 I/O请求运行结束时中断运行。但是,Wi n d o w s对异步I/O请求规定了一个限制,即如果线程将一个异步 I/O请求发送给设备驱动程序,然后终止运行,那么该 I/O请求就会丢失,并且在I/O请求运行结束时,没有线程得到这个通知。在设计良好的线程池中,线程的数量可以根据客户机的需要而增减。因此,如果线程发出一个异步 I/O请求,然后因为线程池缩小而终止运行,那么该 I/O请求也会被
10、撤消。因为这种情况实际上并不是你想要的,所以你需要一个解决方案。如果你想要给发出异步I/O请求的工作项目排队,不能将该工作项目插入线程池的非 I/O组件中。必须将该工作项目放入线程池的 I/O组件中进行排队。该I/O组件由一组线程组成,如果这组线程还有尚未处理的 I/O请求,那么它们决不能终止运行。因此你只能将它们用来运行发出异步I/O请求的代码。若要为I/O组件的工作项目进行排队,仍然必须调用Q u e u e U s e r Wo r k I t e m函数,但是可以为d w F l a g s参数传递W T _ E X E C U T E I N I O T H R E A D。通常只需
11、传递W T _ E X E C U T E D E FA U LT(定义为0),这使得工作项目可以放入非I/O组件的线程中。Wi n d o w s提供的函数(如R e g N o t i f y C h a n g e K e y Va l u e)能够异步执行与非 I/O相关的任务。这些函数也要求调用线程不能终止运行。如果想使用永久线程池的线程来调用这些函数中的一个,可以使用W T _ E X E C U T E I N P E R S I S T E N T T H R E A D标志,它使定时器组件的线程能够执行已排队的工作项目回调函数。由于定时器组件的线程决不会终止运行,因此可以确保
12、最终发生异步操作。应该保证回调函数不会中断,并且保证它能迅速执行,这样,定时器组件的线程就不会受到不利的影响。设计良好的线程池也必须设法保证线程始终都能处理各个请求。如果线程池包含 4个线程,并且有1 0 0个工作项目已经排队,每次只能处理4个工作项目。如果一个工作项目只需要几个毫秒来运行,那么这是不成问题的。但是,如果工作项目需要运行长得多的时间,那么将无法及时处理这些请求。当然,系统无法很好地预料工作项目函数将要进行什么操作,但是,如果知道工作项目需要花费很长的时间来运行,那么可以调用 Q u e u e U s e r Wo r k I t e m函数,为它传递W T _ E X E C
13、 U T E L O N G F U N C T I O N标志。该标志能够帮助线程池决定是否要将新线程添加给线程池。如果线程池中的所有线程都处于繁忙状态,它就会强制线程池创建一个新线程。因此,如果同时对10 000个工作项目进行了排队(使用W T _ E X E C U T E L O N G F U N C T I O N标志),那么这10 000个线程就被添加给该线程池。如果不想创建10 000个线程,必须分开调用Q u e u e U s e r Wo r k I t e m函数,这样某些工作项目就有机会完成运行。线程池不能对线程池中的线程数量规定一个上限,否则就会发生渴求或死锁现象。
14、假如有1 00 0 0个排队的工作项目,当第10 001个项目通知一个事件时,这些工作项目将全部中断运行。如果你已经设置的最大数量为10 000个线程,第10 001个工作项目没有被执行,那么所有的10 000个线程将永远被中断运行。当使用线程池函数时,应该查找潜在的死锁条件。当然,如果工作项目函数在关键代码段、信标和互斥对象上中断运行,那么必须十分小心,因为这更有可能产生死锁现象。始终都应该276计计第二部分编程的具体方法下载了解哪个组件(I/O、非I/O、等待或定时器等)的线程正在运行你的代码。另外,如果工作项目函数位于可能被动态卸载的D L L中,也要小心。调用已卸载的D L L中的函数
15、的线程将会产生违规访问。若要确保不卸载带有已经排队的工作项目的 D L L,必须对已排队工作项目进行引用计数,在调用Q u e u e U s e r Wo r k I t e m函数之前递增计数器的值,当工作项目函数完成运行时则递减该计数器的值。只有当引用计数降为0时,才能安全地卸载D L L。11.2 方案2:按规定的时间间隔调用函数有时应用程序需要在某些时间执行操作任务。Wi n d o w s提供了一个等待定时器内核对象,因此可以方便地获得基于时间的通知。许多程序员为应用程序执行的每个基于时间的操作任务创建了一个等待定时器对象,但是这是不必要的,会浪费系统资源。相反,可以创建一个等待定
16、时器,将它设置为下一个预定运行的时间,然后为下一个时间重置定时器,如此类推。然而,要编写这样的代码非常困难,不过可以让新线程池函数对此进行管理。若要调度在某个时间运行的工作项目,首先要调用下面的函数,创建一个定时器队列:定时器队列对一组定时器进行组织安排。例如,有一个可执行文件控制着若干个服务程序。每个服务程序需要触发定时器,以帮助保持它的状态,比如客户机何时不再作出响应,何时收集和更新某些统计信息等。让每个服务程序占用一个等待定时器和专用线程,这是不经济的。相反,每个服务程序可以拥有它自己的定时器队列(这是个轻便的资源),并且共享定时器组件的线程和等待定时器对象。当一个服务程序终止运行时,它
17、只需要删除它的定时器队列即可,因为这会删除该队列创建的所有定时器。一旦拥有一个定时器队列,就可以在该队列中创建下面的定时器:对于第二个参数,可以传递想要在其中创建定时器的定时器队列的句柄。如果只是创建少数几个定时器,只需要为h Ti m e r Q u e u e参数传递N U L L,并且完全避免调用C r e a t e Ti m e r Q u e u e函数。传递N U L L,会告诉该函数使用默认的定时器队列,并且简化了你的代码。p f n C a l l b a c k和p v C o n t e x t参数用于指明应该调用什么函数以及到了规定的时间应该将什么传递给该函数。d w
18、D u e Ti m e参数用于指明应该经过多少毫秒才能第一次调用该函数(如果这个值是 0,那么只要可能,就调用该函数,使得 C r e a t e Ti m e r Q u e u e Ti m e r函数类似 Q u e u e U s e r Wo r k I t e m)。d w P e r i o d参数用于指明应该经过多少毫秒才能在将来调用该函数。如果为 d w P e r i o d传递0,那么就使它成为一个单步定时器,使工作项目只能进行一次排队。新定时器的句柄通过函数的p h N e w Ti m e r参数返回。工作回调函数必须采用下面的原型:第 11章线程池的使用计计277
19、下载当该函数被调用时,f Ti m e r O r Wa i t F i r e d参数总是T R U E,表示该定时器已经触发。下面介绍C r e a t e Ti m e r Q u e u e Ti m e r的d w F l a g s参数。该参数负责告诉函数,当到了规定的时间时,如何给工作项目进行排队。如果想要让非 I/O组件的线程来处理工作项目,可以使用W T _ E X E C U T E D E FA U LT。如果想要在某个时间发出一个异步I/O请求,可以使用W T _ E X E C U T E I N I O T H R E A D。如果想要让一个决不会终止运行的线程来处
20、理该工作项目,可以使用W T _ E X E C U T E P E R S I S T E N T T H R E A D。如果认为工作项目需要很长的时间来运行,可以使用W T _ E X E C U T E L O N G F U N C T I O N。也可以使用另一个标志,即W T _ E X E C U T E I N T I M E RT H R E A D,下面将介绍它。在表11-1中,能够看到线程池有一个定时器组件。该组件能够创建单个定时器内核对象,并且能够管理它的到期时间。该组件总是由单个线程组成。当调用 C r e a t e Ti m e r Q u e u e Ti m
21、 e r函数时,可以使定时器组件的线程醒来,将你的定时器添加给一个定时器队列,并重置等待定时器内核对象。然后该定时器组件的线程便进入待命睡眠状态,等待该等待定时器将一个 A P C放入它的队列。当等待定时器将该A P C放入队列后,线程就醒来,更新定时器队列,重置等待定时器,然后决定对现在应该运行的工作项目执行什么操作。接着,该线程要检查下面这些标志:W T _ E X E C U T E D E FA U LT、W T _ E X E C U T E I N I O T H R E A D、W T _ E X E C U T E I N P E R S I S T E N T T H R E
22、 A D、W T _ E X E C U T E L O N G F U N C T I O N和W T _E X E C U T E I N T I M E RT H R E A D。不过现在可以清楚地看到 W T _ E X E C U T E D I N T I M E RT H R E A D标志执行的是什么操作:它使定时器组件的线程能够执行该工作项目。虽然这使工作项目的运行效率更高,但是这非常危险。如果工作项目函数长时间中断运行,那么等待定时器的线程就无法执行任何其他操作。虽然等待定时器可能仍然将 A P C项目排队放入该线程,但是在当前运行的函数返回之前,这些工作项目不会得到处理。
23、如果打算使用定时器线程来执行代码,那么该代码应该迅速执行,不应该中断。W T _ E X E C U T E I N I O T H R E A D、W T _ E X E C U T E I N P E R S I S T E N T T H R E A D和W T _ E X E C U T E I N T I M E RT H R E A D等标志是互斥的。如果不传递这些标志中的任何一个(或者使用W T _ E X E C U T E D E FA U LT标志),那么工作项目就排队放入I/O组件的线程中。另外,如果设定了W T _ E X E C U T E I N T I M E R
24、T H R E A D标志,那么W T _ E X E C U T E L O N G F U N C T I O N将被忽略。当不再想要触发定时器时,必须通过调用下面的函数将它删除:即使对于已经触发的单步定时器,也必须调用该函数。h Ti m e r Q u e u e参数指明定时器位于哪个队列中。h Ti m e r参数指明要删除的定时器,句柄通过较早时调用C r e a t e Ti m e r Q u e u e Ti m e r来返回。最后一个参数h C o m p l e t i o n E v e n t告诉你,由于该定时器,什么时候将不再存在没有处理的已排队的工作项目。如果为该
25、参数传递 I N VA L I D _ H A N D L E _ VA L U E,那么在该定时器的所有已排队工作项目完成运行之前,D e l e t e Ti m e r Q u e u e Ti m e r函数不会返回。请想一想这将意味着什么。如果在定时器处理自己的工作项目期间对定时器进行一次中断删除,就会造成一个死锁条件。虽然你正在等待工作项目完成处理操作,但是你在等待它完成操作时却中断了它的处理。只有当线程不是处理定时器的工作项目的线程时,该线程才能进行对定时器的中断删除。另外,如果你正在使用定时器组件的线程,不应该试图对任何定时器进行中断删除,否则278计计第二部分编程的具体方法下
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Windows 核心 编程 011
限制150内