第11章 线程池的使用

第8章讲述了如何使用让线程保持用户方式的机制来实现线程同步的方法。用户方式的同步机制的出色之处在于它的同步速度很快。如果关心线程的运行速度,那么应该了解一下用户方式的同步机制是否适用。

到目前为止,已经知道创建多线程应用程序是非常困难的。需要会面临两个大问题。一个是要对线程的创建和撤消进行管理,另一个是要对线程对资源的访问实施同步。为了对资源访问实施同步,Wi n d o w s提供了许多基本要素来帮助进行操作,如事件、信标、互斥对象和关键代码段等。这些基本要素的使用都非常方便。为了使操作变得更加方便,唯一的方法是让系统能够自动保护共享资源。不幸的是,在
Wi n d o w s提供一种让人满意的保护方法之前,我们已经有了一种这样的方法。

M i c r o s o f t公司的Windows 2000提供了一些新的线程池函数,使得线程的创建、撤消和基本管理变得更加容易。这个新的通用线程池并不完全适合每一种环境,但是它常常可以适合你的需要,并且能够节省大量的程序开发时间。

新的线程池函数使你能够执行下列操作:

• 异步调用函数。

• 按照规定的时间间隔调用函数。

• 当单个内核对象变为已通知状态时调用函数。

• 当异步I / O请求完成时调用函数。

11.1 方案1:异步调用函数

假设有一个服务器进程,该进程有一个主线程,正在等待客户机的请求。当主线程收到该请求时,它就产生一个专门的线程,以便处理该请求。这使得应用程序的主线程循环运行,并等待另一个客户机的请求。这个方案是客户机 /服务器应用程序的典型实现方法。虽然它的实现方法非常明确,但是也可以使用新线程池函数来实现它。

当服务器进程的主线程收到客户机的请求时,它可以调用下面这个函数:

BOOL QueueUserWorkItem(

PTHREAD_START_ROUTINEpfnCallback,

PVOID pvContext,

ULONG dwFlags

);

该函数将一个“工作项目”排队放入线程池中的一个线程中并且立即返回。所谓工作项目是指一个(用
p f n C a l l b a c k参数标识的)函数,它被调用并传递单个参数
p v C o n t e x t。最后,线程池中的某个线程将处理该工作项目,导致函数被调用。所编的回调函数必须采用下面的原型:

DWORD WINAPI WorkItemFunc(PVOID pvContext);

尽管必须使这个函数的原型返回D W O R D,但是它的返回值实际上被忽略了。

注意,你自己从来不调用C r e a t e T h r e a d。系统会自动为你的进程创建一个线程池,线程池中的一个线程将调用你的函数。另外,当该线程处理完客户机的请求之后,该线程并不立即被撤消。它要返回线程池,这样它就可以准备处理已经排队的任何其他工作项目。你的应用程序的运行效率可能会变得更高,因为不必为每个客户机请求创建和撤消线程。另外,由于线程与完成端口相关联,因此可以同时运行的线程数量限制为
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 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) ,并调用函数。当函数返回时,该线程再次调用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请求。当设备驱动程序执行该I / O时,请求排队的线程并没有中断运行,而是继续执行其他指令。异步
I / O是创建高性能可伸缩的应用程序的秘诀,因为它允许单个线程处理来自不同客户机的请求。该线程不必顺序处理这些请求,也不必在等待
I / O请求运行结束时中断运行。

但是,Wi n d o w s对异步I / O请求规定了一个限制,即如果线程将一个异步
I / O请求发送给设备驱动程序,然后终止运行,那么该
I / O请求就会丢失,并且在I / O请求运行结束时,没有线程得到这个通知。在设计良好的线程池中,线程的数量可以根据客户机的需要而增减。因此,如果线程发出一个异步
I / O请求,然后因为线程池缩小而终止运行,那么该
I / O请求也会被撤消。因为这种情况实际上并不是你想要的,所以你需要一个解决方案。

如果你想要给发出异步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。通常只需传递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标志,它使定时器组件的线程能够执行已排队的工作项目回调函数。由于定时器组件的线程决不会终止运行,因此可以确保最终发生异步操作。应该保证回调函数不会中断,并且保证它能迅速执行,这样,定时器组件的线程就不会受到不利的影响。

设计良好的线程池也必须设法保证线程始终都能处理各个请求。如果线程池包含 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 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函数,这样某些工作项目就有机会完成运行。

线程池不能对线程池中的线程数量规定一个上限,否则就会发生渴求或死锁现象。假如有1 00 0 0个排队的工作项目,当第10 001个项目通知一个事件时,这些工作项目将全部中断运行。如果你已经设置的最大数量为10
000个线程,第10 001个工作项目没有被执行,那么所有的10 000个线程将永远被中断运行。

当使用线程池函数时,应该查找潜在的死锁条件。当然,如果工作项目函数在关键代码段、信标和互斥对象上中断运行,那么必须十分小心,因为这更有可能产生死锁现象。始终都应该了解哪个组件(I / O、非I / O、等待或定时器等)的线程正在运行你的代码。另外,如果工作项目函数位于可能被动态卸载的D
L L中,也要小心。调用已卸载的D L L中的函数的线程将会产生违规访问。若要确保不卸载带有已经排队的工作项目的
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提供了一个等待定时器内核对象,因此可以方便地获得基于时间的通知。许多程序员为应用程序执行的每个基于时间的操作任务创建了一个等待定时器对象,但是这是不必要的,会浪费系统资源。相反,可以创建一个等待定时器,将它设置为下一个预定运行的时间,然后为下一个时间重置定时器,如此类推。然而,要编写这样的代码非常困难,不过可以让新线程池函数对此进行管理。

若要调度在某个时间运行的工作项目,首先要调用下面的函数,创建一个定时器队列:

HANDLE CreateTimeQueue();

定时器队列对一组定时器进行组织安排。例如,有一个可执行文件控制着若干个服务程序。每个服务程序需要触发定时器,以帮助保持它的状态,比如客户机何时不再作出响应,何时收集和更新某些统计信息等。让每个服务程序占用一个等待定时器和专用线程,这是不经济的。相反,每个服务程序可以拥有它自己的定时器队列(这是个轻便的资源)
,并且共享定时器组件的线程和等待定时器对象。当一个服务程序终止运行时,它只需要删除它的定时器队列即可,因为这会删除该队列创建的所有定时器。

一旦拥有一个定时器队列,就可以在该队列中创建下面的定时器:

对于第二个参数,可以传递想要在其中创建定时器的定时器队列的句柄。如果只是创建少数几个定时器,只需要为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 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参数返回。

工作回调函数必须采用下面的原型:

VOID WINAPI WaitOrTimerCallback(

PVOID pvContext,

BOOL fTimeOrWaitFired);

当该函数被调用时,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。如果想要让一个决不会终止运行的线程来处理该工作项目,可以使用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 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 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项目排队放入该线程,但是在当前运行的函数返回之前,这些工作项目不会得到处理。如果打算使用定时器线程来执行代码,那么该代码应该迅速执行,不应该中断。

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 RT 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将被忽略。

当不再想要触发定时器时,必须通过调用下面的函数将它删除:

BOOL DeleteTimerQueueTime(

HANDLE hTimeQueue,

HANDLE hTimer,

HANDLE hCompletionEvent);

即使对于已经触发的单步定时器,也必须调用该函数。 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来返回。

另外,如果你正在使用定时器组件的线程,不应该试图对任何定时器进行中断删除,否则就会产生死锁。如果试图删除一个定时器,就会将一个 A P C通知放入该定时器组件的线程队列中。如果该线程正在等待一个定时器被删除,而它不能删除该定时器,那么就会发生死锁。

一旦创建了一个定时器,可以调用下面这个函数来改变它的到期时间和到期周期:

当不再需要一组定时器时,可以调用下面这个函数,删除定时器队列:

该函数取出一个现有的定时器队列的句柄,并删除它里面的所有定时器,这样就不必为删除每个定时器而显式调用D e l e 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参数在这里的语义与它在D e l e t e Ti m e r Q u e u e Ti m e r函数中的语义是相同的。这意味着它存在同样的死锁可能性,因此必须小心。

11.3 方案3:当单个内核对象变为已通知状态时调用函数

M i c r o s o f t发现,许多应用程序产生的线程只是为了等待内核对象变为已通知状态。一旦对象得到通知,该线程就将某种通知移植到另一个线程,然后返回,等待该对象再次被通知。有些编程人员甚至编写了代码,在这种代码中,若干个线程各自等待一个对象。这对系统资源是个很大的浪费。当然,与创建进程相比,创建线程需要的的开销要小得多,但是线程是需要资源的。每个线程有一个堆栈,并且需要大量的
C P U指令来创建和撤消线程。始终都应该尽量减少它使用的资源。

如果想在内核对象得到通知时注册一个要执行的工作项目,可以使用另一个新的线程池函数:

该函数负责将参数传送给线程池的等待组件。你告诉该组件,当内核对象(用 h O b j e c t进行标识)得到通知时,你想要对工作项目进行排队。也可以传递一个超时值,这样,即使内核对象没有变为已通知状态,也可以在规定的某个时间内对工作项目进行排队。超时值
0和I N F I N I T E是合法的。一般来说,该函数的运行情况与
Wa i t F o r S i n g l e O b j e c t函数(第9章已经介绍)相似。当注册了一个等待组件后,该函数返回一个句柄(通过
p h N e w Wa i t O b j e c t参数)以标识该等待组件。

在内部,等待组件使用Wa i t F o r M u l t i p l e O b j e c t s函数来等待已经注册的对象,并且要受到该函数已经存在的任何限制的约束。限制之一是它不能多次等待单个句柄。因此,如果想要多次注册单个对象,必须调用D
u p l i c a t e H a n d l e函数,并对原始句柄和复制的句柄分开进行注册。当然,Wa i t F o r M u l t i p l e O b j e c t s能够等待已通知的对象中的任何一个,而不是所有的对象。如果熟悉Wa
i t F o r M u l t i p l e O b j e c t s函数,那么一定知道它一次最多能够等待
6 4(M A X I M U M _WA I T _ O B J E C T S)个对象。如果用R e g i s t e
r Wa i t F o r S i n g l e O b j e c t函数注册的对象超过6 4个,那么将会出现什么情况呢?这时等待组件就会添加另一个也调用
Wa i t F o r M u l t i p l e O b j e c t s函数的线程。实际上,每隔6 3个对象后,就要将另一个线程添加给该组件,因为这些线程也必须等待负责控制超时的等待定时器对象。

当工作项目准备执行时,它被默认排队放入非 I / O组件的线程中。这些线程之一最终将会醒来,并且调用你的函数,该函数的原型必须是下面的形式:

VOID WINAPI WaitOrTimerCallbackFunc(

PVOID pvContext,

BOOLEAN fTimerOrWaitFired

);

如果等待超时了,f Ti m e r O r Wa i t F i r e d参数的值是T R U E。如果等待时对象变为已通知状态,则该参数是FA
L S E。

对于R e g i s t e r Wa i t F o r S i n g l e O b j e c t函数的d w F l a g s参数,可以传递
W T _ E X E C U T E I N -WA I T T H R E A D,它使等待组件的线程之一运行工作项目函数本身。它的运行速率更高,因为工作项目不必排队放入
I / O组件中。但是这样做有一定的危险性,因为正在执行工作项目的等待组件函数的线程无法等待其他对象得到通知。只有当工作项目函数运行得很快时,才应该使用该标志。

如果工作项目将要发出异步 I / O请求,或者使用从不终止运行的线程来执行某些操作,那么也可以传递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 L O N G F U N C T I O N标志来告诉线程池,你的函数可能要花费较长的时间来运行,而且它应该考虑将一个新线程添加给线程池。只有当工作项目正在被移植到非
I / O组件或I / O组件中时,才能使用该标志,如果使用等待组件的线程,不应该运行长函数。

应该了解的最后一个标志是W T _ E X E C U T E O N LY O N C E。假如你注册一个等待进程内核对象的组件,一旦该进程对象变为已通知状态,它就停留在这个状态中。这会导致等待组件连续地给工作项目排队。对于进程对象来说,可能不需要这个行为特性。如果使用
W T _E X E C U T E O N LY O N C E标志,就可以防止出现这种情况,该标志将告诉等待组件在工作项目执行了一次后就停止等待该对象。

现在,如果正在等待一个自动重置的事件内核对象。一旦该对象变为已通知状态,该对象就重置为它的未通知状态,并且它的工作项目将被放入队列。这时,该对象仍然处于注册状态,同时,等待组件再次等待该对象被通知,或者等待超时(它已经重置)结束。当不再想让该等待组件等待你的注册对象时,必须取消它的注册状态。即使是使用 W T _ E X E C U T E O N LY O N C
E标志注册的并且已经拥有队列的工作项目的等待组件,情况也是如此。调用下面这个函数,可以取消等待组件的注册状态:

BOOL UnregisterWaitEx(

HANDLE hWaitHandle,

HANDLE hCompIetionEvent

);

第一个参数指明一个注册的等待(由 R e g i s t e r Wa i t F o r S i n g l e O b j e c t返回) ,第二个参数指明当已注册的、正在等待的所有已排队的工作项目已经执行时,你希望如何通知你。与D
e l e t e Ti m e r Q u e u e Ti m e r函数一样,可以传递
N U L L(如果不要通知的话) ,或者传递I N VA L I D _ H A N D L E _ VA L U E(中断对函数的调用,直到所有排队的工作项目都已执行) ,也可以传递一个事件对象的句柄(当排队的工作项目已经执行时,它就会得到通知)
。对于无中断的函数调用,如果没有排队的工作项目,那么U n r e g i s t e r Wa i t E x返回T R U E,否则它返回FA
L S E,而G e t L a s t E r r o r返回S TAT U S _ P E N D I N G。

同样,当你将I N VA L I D _ H A N D L E _ VA L U E传递给U n r e g i s t e r Wa i t E x时,必须小心避免死锁状态。在试图取消等待组件的注册状态,从而导致工作项目运行时,该工作项目函数不应该中断自己的运行。这好像是说:暂停我的运行,直到我完成运行为止一样——这会导致死锁。然而,如果等待组件的线程运行一个工作项目,而该工作项目取消了导致工作项目运行的等待组件的注册状态,U
n r e g i s t e r Wa i t E x是可以用来避免死锁的。还有一点需要说明,在取消等待组件的注册状态之前,不要关闭内核对象的句柄。这会使句柄无效,同时,等待组件的线程会在内部调用Wa i t F o r M u l t i p l e O b j e c t s函数,传递一个无效句柄。Wa
i t F o r M u l t i p l e O b j e c t s的运行总是会立即失败,整个等待组件将无法正常工作。

最后,不应该调用P u l s e E v e n t函数来通知注册的事件对象。如果这样做了,等待组件的线程就可能忙于执行某些别的操作,从而错过了事件的触发。这不应该是个新问题了。P u l s e E v e n t几乎能够避免所有的线程结构产生这个问题。

11.4 方案4:当异步I / O请求完成运行时调用函数

最后一个方案是个常用的方案,即服务器应用程序发出某些异步 I / O请求,当这些请求完成时,需要让一个线程池准备好来处理已完成的
I / O请求。这个结构是I / O完成端口原先设计时所针对的一种结构。如果要管理自己的线程池,就要创建一个
I / O完成端口,并创建一个等待该端口的线程池。还需要打开多个
I / O设备,将它们的句柄与完成端口关联起来。当异步
I / O请求完成时,设备驱动程序就将“工作项目”排队列入该完成端口。

这是一种非常出色的结构,它使少数线程能够有效地处理若干个工作项目,同时它又是一种很特殊的结构,因为线程池函数内置了这个结构,使你可以节省大量的设计和精力。若要利用这个结构,只需要打开设备,将它与线程池的非 I / O组件关联起来。记住,I
/ O组件的线程全部在一个I / O组件端口上等待。若要将一个设备与该组件关联起来,可以调用下面的函数:

该函数在内部调用 C r e a t e I o C o m p l e t i o n P o r t,传递h D e v i c e和内部完成端口的句柄。调用B
i n d I o C o m p l e t i o n C a l l b a c k也可以保证至少有一个线程始终在非
I / O组件中。与该设备相关联的完成关键字是重叠完成例程的地址。这样,当该设备的
I / O运行完成时,非I / O组件就知道要调用哪个函数,以便它能够处理已完成的I / O请求。该完成例程必须采用下面的原型:

VOID WAINAPI OverlappedCompletionRoutine(

DWORD dwErrorCode,

DWORD dwNumberOfBytesTransferred,

POVERLAPPED pOverlapped

);

你将会注意到没有将一个 O V E R L A P P E D结构传递给
B i n d I o C o m p l e t i o n C a l l b a c k。O V E R L A P P E D结构被传递给R
e a d F i l e和Wr i t e F i l e之类的函数。系统在内部始终保持对这个带有待处理I / O请求的重叠结构进行跟踪。当该请求完成时,系统将该结构的地址放入完成端口,从而使它能够被传递给你的O
v e r l a p p e d C o m p l e t i o n R o u t i n e函数。另外,由于该完成例程的地址是完成的关键,因此,如果要将更多的上下文信息放入
O v e r l a p p e d C o m p l e t i o n R o u t i n e函数,应该使用将上下文信息放入O V E R L A P P E D结构的结尾处的传统方法。

还应该知道,关闭设备会导致它的所有待处理的 I / O请求立即完成,并产生一个错误代码。要作好准备,在你的回调函数中处理这种情况。如果关闭设备后你想确保没有运行任何回调函数,那么必须引用应用程序中的计数特性。换句话说,每次发出一个
I / O请求时,必须使计数器的计数递增,每次完成一个I / O请求,则递减计数器的计数。

目前没有特殊的标志可以传递给B i n d I o C o m p l e t i o n C a l l b a c k函数的d w F l a g s参数,因此必须传递0。相信你能够传递的标志是
W T _ E X E C U T E I N I O T H R E A D。如果一个I / O请求已经完成,它将被排队放入一个非I
/ O组件线程。在O v e r l a p p e d C o m p l e t i o n R o u t i n e函数中,可以发出另一个异步I / O请求。但是记住,如果发出I
/ O请求的线程终止运行,该I / O请求也会被撤消。另外,非I / O组件中的线程是根据工作量来创建或撤消的。如果工作量很小,该组件中的线程就会终止运行,其
I / O请求仍然处于未处理状态。如果
B i n d I o C o m p l e t i o n C a l l b a c k函数支持W T _ E X E C U T E I N I O T H R E A D标志,那么在完成端口上等待的线程就会醒来,并将结果移植到一个I
/ O组件的线程中。由于在I / O请求处于未处理状态下时这些线程决不会终止运行,因此可以发出I / O请求而不必担心它们被撤消。

虽然W T _ E X E C U T E I N I O T H R E A D标志的作用不错,但是可以很容易模仿刚才介绍的行为特性。在
O v e r l a p p e d C o m p l e t i o n R o u t i n e函数中,只需要调用
Q u e u e U s e r Wo r k I t e m,传递W T _ E X E C U T E I N I O T H R E A D标志和想要的任何数据(至少是重叠结构) 。这就是线程池函数能够为你执行的全部功能。

总结:线程池相关:

1.异步调用函数

2.按规定的时间间隔调用函数

3.当单个内核对象变为已通知状态时调用函数

4.当异步I / O请求完成运行时调用函数

Windows核心编程 第十一章 线程池的使用的更多相关文章

  1. Windows核心编程 第七章 线程的调度、优先级和亲缘性(下)

    7.6 运用结构环境 现在应该懂得环境结构在线程调度中所起的重要作用了.环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的C P U时,它就能够找到它上次中断运行的地方. 知道这样低层 ...

  2. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  3. windows核心编程---第六章 线程的调度

    每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在CONTEXT结构的值载入c ...

  4. Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)

    第7章 线程的调度.优先级和亲缘性 抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间.本章将要介绍Microsoft Windows 98和Windows 2000使用的一些算 ...

  5. windows核心编程---第五章 线程的基础

    与前面介绍的进程一样,线程也有两部分组成.一个是线程内核对象.它是一个数据结构,操作系统用它来管理线程以及用它来存储线程的一些统计信息.另一个是线程栈,用于维护线程执行时所需的所有函数参数和局部变量. ...

  6. Windows核心编程 第六章 线程基础知识 (下)

    6.6 线程的一些性质 到现在为止,讲述了如何实现线程函数和如何让系统创建线程以便执行该函数.本节将要介绍系统如何使这些操作获得成功. 图6 - 1显示了系统在创建线程和对线程进行初始化时必须做些什么 ...

  7. windows核心编程 第8章201页旋转锁的代码在新版Visual Studio运行问题

    // 全局变量,用于指示共享的资源是否在使用 BOOL g_fResourceInUse = FALSE; void Func1() { //等待访问资源 while(InterlockedExcha ...

  8. windows核心编程 第5章job lab示例程序 解决小技巧

    看到windows核心编程 第5章的最后一节,发现job lab例子程序不能在我的系统(win8下)正常运行,总是提示“进程在一个作业里”         用process explorer程序查看 ...

  9. 《Windows核心编程》第十一章——线程池

    隐式使用工作项 #include <iostream> #include <windows.h> ; VOID NTAPI SimpleCallback(PTP_CALLBAC ...

随机推荐

  1. WPF 应用 - WPF 播放 GIF 的两种方式

    1. 使用 Winform 的 PictureBox 1.1 引用 dll WindowsFormsIntegration.dll System.Windows.Forms.dll System.Dr ...

  2. C语言II博客作业01

    这个作业属于那个课程 https://edu.cnblogs.com/campus/zswxy/SE2020-4 这个作业要求在哪里 https://edu.cnblogs.com/campus/zs ...

  3. Nodejs学习笔记(1) Nodejs安装+借助express模块简单部署服务器

    1 安装 1.1 下载和安装 1.2 什么是REPL?如何使用? 1.3 npm对单一模块的安装和删除功能 1.4 通过package.json自定义模块(安装模块) 1.5 设置全局目录 2 部署网 ...

  4. HarmonyOS三方件开发指南(14)-Glide组件功能介绍

    <HarmonyOS三方件开发指南>系列文章合集 引言 在实际应用开发中,会用到大量图片处理,如:网络图片.本地图片.应用资源.二进制流.Uri对象等,虽然官方提供了PixelMap进行图 ...

  5. 【vue开发】watch中的immediate与deep的使用

    下面是我对于immediate与deep的理解 1.immediate 设置immediate为true后,监听会在被监听值初始化的时候就开始,也就页面上的数据还未变化的时候. 经过测试发现,如果监听 ...

  6. Trie、并查集、堆、Hash表学习过程以及遇到的问题

    Trie.并查集.堆.Hash表: Trie 快速存储和查找字符串集合 字符类型统一,将单词在最后一个字母结束的位置上打上标记 练习题:Trie字符串统计 import java.util.*; pu ...

  7. java例题_29 二维数组问题,并输出对角线之和

    1 /*29 [程序 29 求矩阵对角线之和] 2 题目:求一个 3*3 矩阵对角线元素之和 3 程序分析:利用双重 for 循环控制输入二维数组,再将 a[i][i]累加后输出. 4 */ 5 6 ...

  8. Spring笔记(三)

    Spring AOP 一.AOP(概念) 1. 什么是AOP 面向切面编程(方面),利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高了 ...

  9. 当初自学C++时的笔记记录

    编辑:刘风琛 最初编写日期:2020年4月11日下午 最新更新日期:2020年9月20日上午 标注: 从笔记开始截止到程序第四章"程序流程结构",使用Joplin编写,其余部分为T ...

  10. 【Java】 5.0 判断与转换

    [概述] 在if/条件语句中,我们已经谈及判断了,这次将详细讲讲一些逻辑判断 基本逻辑 &:且,And,需要二者均为True |:或,Or ,需要二者其一为False即可 ^:异或,XOE,两 ...