《Windows via C/C++》学习笔记 (八) Windows 线程池 纤程

线程池(thread pool),允许有多个线程同时存在,并发执行,并且这些线程受到统一管理。

  在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,下同):

VOID NTAPI SimpleCallback(           // 函数名可以任意
     PTP_CALLBACK_INSTANCE pInstance,
     PVOID pvContext);     
// 函数参数

 

  然后,你可以提交一个请求给线程池,让其中的一个线程执行这个函数:

BOOL TrySubmitThreadpoolCallback(
   PTP_SIMPLE_CALLBACK pfnCallback,   
// 按上面格式定义的异步函数的指针
   PVOID pvContext,           // 传递给异步函数的参数
   PTP_CALLBACK_ENVIRON pcbe);

 

  TrySubmitThreadpoolCallback 函数在线程池队列中加入一个“工作项”(work item),如果成功返回TRUE,否则返回FLASE。pcbe参数下面会介绍,你可以简单地传递NULL给这个参数(下同)。

  你不必调用CreateThread来创建线程,当进程内调用TrySubmitThreadpoolCallback函数的时候,系统会自动地给你的进程创建线程池,然后在该线程池队列中队列中加入一个“工作项”,并让其中的一个线程来执行你定义的异步函数。当异步函数执行完毕后,该线程不会被销毁,而是进入线程池等待另一个“工作项”的到来。线程池中的线程是回收利用的,并不是不断创建和销毁的,这样提高了性能。同时,这个线程池如果觉得自己的线程太多的话,就自动地销毁一些线程,可以让性能达到最佳。

  你可以使用CreateThreadpoolWork函数来创建一个“工作项”:

PTP_WORK CreateThreadpoolWork(
   PTP_WORK_CALLBACK pfnWorkHandler,     
// 异步函数指针
   PVOID pvContext,                       // 异步函数参数
   PTP_CALLBACK_ENVIRON pcbe);

 

  该函数接受一个异步函数的指针和这个异步函数的参数,并创建一个用户模式的数据结构来保存对应的3个参数的数据,同时返回一个指向这个数据结构的指针,可以理解为“工作项”指针。

  其中,pfnWordHandler 函数是一个异步函数的指针,这个异步函数会被线程池中某个线程调用,该异步函数定义如下:

VOID CALLBACK WorkCallback(      // 函数名可以任意
   PTP_CALLBACK_INSTANCE Instance,
   PVOID Context,      
// 异步函数参数,由CreateThreadpoolWork函数指定
   PTP_WORK Work);      // 线程池“工作项”指针

 

  当你想将一个创建了的工作项提交给线程池,可以使用SubmitThreadpoolWork函数:

VOID SubmitThreadpoolWork(PTP_WORK pWork);      // 参数是工作项指针

 

  如果多次调用该函数向一个线程池提交同一个工作项,那么异步函数会被调用多次,而每次的参数都是同样一个值。

  如果有另一个线程想要取消提交的工作项,或者挂起自己等待工作项完成,可以使用这个函数:

VOID WaitForThreadpoolWorkCallbacks(
   PTP_WORK pWork,     
// 工作项指针
   BOOL bCancelPendingCallbacks);   // 是否取消该工作项

 

  pWork 函数是一个工作项指针,由函数CreateThreadpoolWork创建并返回。如果该工作项没有被提交,则该函数马上返回,不做任何工作。

  如果传递TRUE给参数bCancelPendingCallbacks,WaitForThreadpoolWorkCallbacks函数将试图取消这个先前提交的工作项。如果这个工作项正在被处理,那么这个处理不会被打断,该函数会等待直到工作项结束才返回。如果这个工作项被提交,但是目前不在处理,那么该函数就会立即取消该工作项并理解返回,那么这个工作项的异步函数就不会被调用了。

  如果传递FALSE给传递bCancelPendingCallbacks,WaitForThreadpoolWorkCallbacks函数将挂起这个调用它的线程,直到指定的工作项完成,而线程池中执行这个工作项的线程在完成处理工作项之后返回线程池,继续处理下一个工作项。

  如果传递给WaitForThreadpoolWorkCallbacks函数的第一个参数的工作项指针被提交给线程池多次,也就是说多个工作项使用同一个工作项指针,如果第2个参数为FALSE,那么WaitForThreadpoolWorkCallbacks将等到这个工作项指针代表的所有工作项处理完成才返回。如果传递TRUE给第2个参数,WaitForThreadpoolWorkCallbacks将等待,只要当前正在执行的工作项结束就返回。

  当你不要使用工作项的时候,使用CloseThreadpoolWork函数关闭之。 

VOID CloseThreadpoolWork(PTP_WORK pwk);

 

定时调用一个函数

  这是Windows线程池提供的第2个功能。

  有的时候,应用程序需要在某一个特定的时间执行特定的任务,你可以选择使用Windows内核对象“等待定时器”来实现这个功能,但是如果这种基于时间的任务特别的多,那么就不得不为每个这样的任务创建一个“等待定时器”对象,无疑会浪费资源。当然,你也许会想到创建单个“等待定时器”,然后不断地设置它的下一次要等待的时间,这样就可以完成多个基于时间的任务了。但是如此一来,代码量就会增大。

  Windows提供了线程池来实现这样的功能,其方法是通过“线程池定时器”。

 

  首先,你要定义一个如下格式的回调函数,让线程池中的线程定时调用它:

VOID CALLBACK TimeoutCallback(      //  函数名可以任意
   PTP_CALLBACK_INSTANCE pInstance,
   PVOID pvContext,         
//  函数的参数
   PTP_TIMER pTimer);        //  一个指向“线程池定时器”的指针

 

  然后告诉线程池什么时候调用你的回调函数:

PTP_TIMER CreateThreadpoolTimer(
   PTP_TIMER_CALLBACK pfnTimerCallback,   
//  类似上面格式的函数的指针
   PVOID pvContext,                //  回调函数的参数,由这个参数指明
   PTP_CALLBACK_ENVIRON pcbe);

 

  不难发现,CreateThreadpoolTimer函数和第1种方法中的CreateThreadpoolWork函数十分类似,而且两者的回调函数也十分类似。当调用CreateThreadpoolTimer函数的时候,第1个参数指向一个回调函数,第2个参数pvContext会传递给这个回调函数的第2个参数,而其返回值——一个“线程池定时器”指针也会传递给这个回调函数的第3个参数。

  如果你想把由CreateThreadpoolTimer函数创建的“线程池定时器”注册到线程池中去,可以使用如下函数:

VOID SetThreadpoolTimer(
   PTP_TIMER pTimer,        
//  一个“线程池定时器”指针
   PFILETIME pftDueTime,     //  回调函数被调用的时间
   DWORD msPeriod,           //  周期性调用回调函数的间隔时间(毫秒)
   DWORD msWindowLength);    //  周期时间的波动范围(毫秒)

 

  该函数的第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函数:

BOOL IsThreadpoolTimerSet(PTP_TIMER pti);

 

  你也可以让线程等待一个“线程池定时器”完成工作,呼叫函数WaitForThreadpoolTimerCallbacks,当要关闭一个“线程池定时器”的时候,呼叫函数CloseThreadpoolTimer,这两个函数同前面讨论的WaitForThreadpoolWork和CloseThreadpoolWorkCallbacks函数类似,可以参考本篇前面的内容。

 

  下面总结一下“线程池定时器”的使用方法:

  1. 定义一个回调函数,如TimeoutCallback那样的格式。
  2. 使用CreateThreadpoolTimer函数创建一个“线程池定时器”,并将已定义的回调函数与它关联在了一起。
  3. 使用SetThreadpoolTimer设置“线程池定时器”的属性,并将其提交给线程池。
  4. 调用CloseThreadpoolTimer关闭“线程池定时器”。

 

当一个内核对象被通知的时候调用一个函数

  有很多线程,初始化的时候等待一个内核对象,一旦这个内核对象转入“已通知”状态,线程就会通知另外一些线程,然后转回继续等待这个内核对象。但是,如果这样的线程很多的话,无疑会增大系统的开销。

  此时,你可以考虑使用线程池来实现这个功能,就是当一个内核对象被通知的时候,由线程池中的一个线程调用一个异步的回调函数。

   如果你想让一个“工作项”在一个内核对象为“已通知”的状态下被执行,这个基本流程和前面两个功能的流程类似。

  首先,定义一个如下格式的异步函数:

VOID CALLBACK WaitCallback(      //  函数名可以任意
   PTP_CALLBACK_INSTANCE pInstance,
   PVOID Context,          
//  函数的参数
   PTP_WAIT Wait,          //  线程池等待对象的指针
   TP_WAIT_RESULT WaitResult);      //  该函数被调用的原因

 

  然后,需要创建一个“线程池等待对象”:

PTP_WAIT CreateThreadpoolWait(
   PTP_WAIT_CALLBACK    pfnWaitCallback,     
//  回调函数指针,函数如上定义
   PVOID                pvContext,           //  传递给回调函数参数Context
   PTP_CALLBACK_ENVIRON pcbe);

 

  接着就可以将创建的“线程池等待对象”与这个线程池关联起来,此时线程池队列中会有一个“等待项”记录:

VOID SetThreadpoolWait(
   PTP_WAIT  pWaitItem,     
//  一个“线程池等待对象”指针
   HANDLE    hObject,        //  一个内核对象句柄,当被通知时,回调函数被调用
   PFILETIME pftTimeout);    //  等待hObjetct内核对象受到通知的时间

 

  这个函数的第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是类似的。

 

当异步I/O请求结束的时候调用一个函数

  读过上面3中线程池的功能,不难发现有很多共同的特点,连函数名称都很有规律。线程池中的线程由系统统一管理,自动地创建和销毁。其实,这些线程内部都在等待一个I/O完成端口,这个I/O完成端口称为“线程池的I/O完成端口”。

  如果你要使用线程池来处理设备异步I/O请求的时候,当你打开一个设备的时候,必须首先将这个设备与“线程池I/O完成端口”关联起来,然后告诉线程池当设备异步I/O请求结束之后哪个函数将被调用。

 

  首先,定义一个如下格式的异步回调函数:

复制代码
VOID CALLBACK OverlappedCompletionRoutine(      //  函数名可以任意
    PTP_CALLBACK_INSTANCE pInstance,
    PVOID          pvContext,     
//  该函数的一个参数
    PVOID          pOverlapped,   //  OVERLAPPED结构指针
    ULONG         IoResult,         //  I/O请求结果,如果成功,则为NO_ERROR
    ULONG_PTR  NumberOfBytesTransferred,      //  I/O请求的数据传输字节数
    PTP_IO         pIo);              //  一个“线程池I/O完成项”指针
复制代码

 

  这个函数的最后一个参数pIo是一个PTP_IO类型,即一个线程池I/O完成项,它与“线程池工作项”和“线程池等待项”是类似的。你必须创建它,使用如下函数:

PTP_IO CreateThreadpoolIo(
    HANDLE       hDevice,     
//  与线程池I/O完成端口关联的设备对象句柄
    PTP_WIN32_IO_CALLBACK pfnIoCallback,      //  如上格式的异步回调函数指针
    PVOID        pvContext,    //  该参数在调用时传递给回调函数的第2个参数
    PTP_CALLBACK_ENVIRON  pcbe);

 

  该函数将hDevice参数所对应的设备记录到线程池I/O项中,然后,可以使用如下函数将设备与线程池I/O完成端口关联起来:

VOID StartThreadpoolIo(PTP_IO pio);

 

  注意,StartThreadpoolIo函数必须在ReadFile和WriteFile之前调用,如果没有在它们之前调用,你的异步回调函数不会被调用。

  当你想停止调用回调函数的时候,可以使用CancelThreadpoolIo,如果在调用ReadFile或WriteFile之后,它们的返回值是FLASE,而GetLastError的返回值不是ERROR_IO_PENDING,那么也应该调用CancelThreadpoolIo:

VOID CancelThreadpoolIo(PTP_IO pio);

 

  当结束了设备I/O,你应该使用CloseHandle关闭设备句柄,然后呼叫CloseThradpoolIo关闭线程池I/O项,即取消设备与线程池I/O请求的关联。

VOID CloseThreadpoolIo(PTP_IO pio);

 

  另外,你可以让一个线程等待I/O请求结束:

VOID WaitForThreadpoolIoCallbacks(
   PTP_IO pio,
   BOOL bCancelPendingCallbacks);     
//  是否取消回调函数的调用

 

  如果给这个函数的参数BCancelPendingCallbacks传递TRUE,那么回调函数将不会被调用,该函数的用法和WaitForThreadpoolWork是类似的。

 

回调函数结束之后的行为

  注意上面讨论的各种类型的回调函数第1个参数,是一个PTF_CALLBACK_INSTANCE类型的数据pInstance,从字面上看,是“线程池回调函数实体指针”,也就是说,这个数据是各个回调函数唯一的,是回调函数的标识,这个数据是在调用回调函数之前由系统自动分配的,可以用这个参数调用如下函数:

 

复制代码
//  当回调函数返回的时候,自动离开一个指定的关键代码段
VOID LeaveCriticalSectionWhenCallbackReturns(
     PTP_CALLBACK_INSTANCE pci,     
//  回调函数实体指针,标识一个回调函数
     PCRITICAL_SECTION pcs);       //  关键代码段结构指针,标识一个关键代码段

//  当回调函数返回的时候,自动释放一个指定的互斥内核对象
VOID ReleaseMutexWhenCallbackReturns(
     PTP_CALLBACK_INSTANCE pci,
     HANDLE mut);

//  当回调函数返回的时候,自动释放一个指定的信号量内核对象
VOID ReleaseSemaphoreWhenCallbackReturns(
     PTP_CALLBACK_INSTANCE pci,
     HANDLE sem,
     DWORD crel);

//  当回调函数返回的时候,自动将一个事件内核对象设置为已通知状态
VOID SetEventWhenCallbackReturns(
     PTP_CALLBACK_INSTANCE pci,
     HANDLE evt);

//  当回调函数返回的时候,自动卸载一个模块
VOID FreeLibraryWhenCallbackReturns(
     PTP_CALLBACK_INSTANCE pci,
     HMODULE mod);
复制代码

 

  这些函数的第1个参数pci标识当线程池前正在处理的工作、定时器、等待、I/O项,调用这些函数,表示对应的回调函数结束之后,所做的一些释放和设置工作。

  其中,前4个函数,提供了一种方法来通知其他线程,说明线程池中的某一个工作项完成。最后一个函数,提供了一种方法来卸载DLL的方法,特别是当回调函数是从DLL中导出的时候,这种方法特别适用。要注意的是,只能有一个动作在回调函数返回的时候被执行,你不能多次呼叫上述5个函数,这样的话,最后一次呼叫的函数会覆盖前面所呼叫的函数,因此,不能同时离开关键代码段并释放信号量内核对象。

  另外,还有两个函数需要回调函数实体指针:

BOOL CallbackMayRunLong(PTP_CALLBACK_INSTANCE pci);
VOID DisassociateCurrentThreadFromCallback(PTP_CALLBACK_INSTANCE pci);

 

  CallbackMayRunLong并不是设置回调函数结束时的工作的,而是当一个回调函数认为自己执行的时间可能比较长才可能需要呼叫这个函数。此时,线程池不会创建新的线程,以此来提高这个回调函数的性能。当该函数返回FLASE,线程池不允许其他线程能够处理线程池队列中的其他项;如果返回TRUE,表示线程池允许其他线程处理其他工作项。

  DisassociateCurrentThreadFromCallback函数表明与回调函数关联的工作项在“逻辑上”完成了(其实不是真正完成),此时允许等待在这个工作项上的函数返回,比如WaitForThreadpoolWorkCallbacks、WaitForThreadpoolTimerCallbacks、WaitForThreadpoolWaitCallbacks、WaitForThreadpoolIoCallbacks这些函数返回。

 

定制线程池

  以上所讨论的线程池,都是系统自动控制的,用户无法改变其内部的流程。

  下面,我们讨论一下如何自己定制线程池。

  你会注意到,上面的每个“创建”函数:CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait、CreateThreadpoolIo以及TrySubmitThreadpoolCallback这5个函数中的最后一个参数pcbe,一个类型为PTP_CALLBACK_ENVIRON的参数,一个指向“回调函数环境”结构的指针。你可以简单地传递NULL给这个参数,表明你使用默认的系统自动分配和管理的进程线程池。

  但是,有的时候程序员喜欢自己来控制线城池,给线城池设置一些规则和属性。比如设置改线城池中线程的数量上下限,或者想操纵线城池中线程的创建和销毁。

  要达到这个目的,可以自己创建线程池,然后设置一些属性。

  首先,创建一个线程池,使CreateThreadpool函数:

PTP_POOL CreateThreadpool(PVOID reserved);   //  参数是保留参数,必须为NULL

 

  该函数返回一个PTP_POOL类型的数据,姑且认为是“线城池指针”的意思,即代表了一个线程池。

  然后,就可以通过这个线城池指针来呼叫相应的API函数,设置线程池的一些属性了。

  你可以设置线城池的线程数量的上下限:

复制代码

BOOL SetThreadpoolThreadMinimum(

     PTP_POOL pThreadPool,

     DWORD cthrdMin);


BOOL SetThreadpoolThreadMaximum(

     PTP_POOL pThreadPool,

     DWORD cthrdMost);

 

复制代码

 

  通过呼叫这两个函数之后,线程池中的线程的数量决不会少于设置的最小值,并且允许这个数量增大到最大值。顺便说一下,默认的线城池的线程数量范围为1~500。

  当一个线程池需要关闭的时候,呼叫CloseThreadpool函数:

VOID CloseThreadpool(PTP_POOL pThreadPool);

 

  呼叫这个函数之后,对应的线程池队列中的项都不会被处理,当前正在处理工作项的线程都会结束处理然后线程终止,其他还没有被处理的项都会被取消。

  一旦你创建了你自己的线程池并设置了线程数量上下限,你就初始化那个“回调函数环境”结构了,该结构中包含了另外的一些设置。这个结构与一个工作项有关。该结构定义如下:

复制代码
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(PTP_CALLBACK_ENVIRON pcbe);

 

  该函数将结构中的各个字段设置为0,除了Version被设置为1。

  当你不再需要使用该结构的时候,使用如下函数删除它:

VOID DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);

 

   在初始化TP_CALLBACK_ENVIRON结构之后,该结构与一个工作项相关,然后你就可以使用这个结构来提交一个工作项给指定的线程池了:

VOID SetThreadpoolCallbackPool(
     PTP_CALLBACK_ENVIRON pcbe, 
//  回调函数环境结构指针
     PTP_POOL pThreadPool);      //  线程池指针,由CreateThreadpool函数返回

 

  如果不调用该函数,那么回调函数环境结构中的Pool成员的值就是NULL,表示不为任何定制的线程池相关,那么与此结构相关的工作项就会提交给默认线程池。

  如果一个工作项处理的时间比较长,可以调用SetThreadpoolCallbackRunsLong函数,这会导致线程池更快地创建线程来处理这个工作项。

VOID SetThreadpoolCallbackRunsLong(PTP_CALLBACK_ENVIRON pcbe);

 

  你可以呼叫SetThreadpoolCallbackLibrary来确保当某个工作项未完成的时候,一个DLL始终被加载到进程地址中。该函数也可以除去潜在的死锁。

VOID SetThreadpoolCallbackLibrary(
     PTP_CALLBACK_ENVIRON pcbe,
     PVOID mod);     
//  模块指针,就是模块句柄

 

  线程池要处理很多的项目,因此很难确定这些项目什么时候处理结束,这也使得线程池的清除工作困难了。为了解决这种情况,线程池提供了一种机制——“清理组”。要注意的是,该机制不适合于默认线程池,只适合于定制的线程池。因为默认线程池的生命期与进程一样,系统会在进程结束之后清除默认线程池。

  为了使用“清理组”机制,首先,你需要创建一个清理组:

PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();

 

  然后将已创建的清理组与线程池关联起来:

VOID SetThreadpoolCallbackCleanupGroup(
  PTP_CALLBACK_ENVIRON pcbe,     
//  回调函数环境结构指针
  PTP_CLEANUP_GROUP ptpcg,         //  清理组指针
  PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng);      //  回调函数指针


  该函数在内部将回调函数指针p的cbe参数CleanupGroup和CleanupCancelCallback成员设置为第2和第3个参数所提供的值。如果清理组被取消,第3个参数pfng表示的回调函数将会被调用,这个回调函数必须满足如下格式:

VOID CALLBACK CleanupGroupCancelCallback(      //  函数名可以任意
  PVOID pvObjectContext,
  PVOID pvCleanupContext);


  每次你调用CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait和CreateThreadpoolIo的时候,如果传递给它们的最后一个参数pcbe不是NULL,那么一项就会加入相关的组清理中,当这样的项都完成之后,你调用CloseThreadpoolWork、CloseThreadpoolTimer、CloseThreadpoolWait、ClsoeThreadpoolIo的时候,又暗中地将这样的项从组清理中删除了。

  这个时候,想要删除线程池,可以调用如下函数:

VOID CloseThreadpoolCleanupGroupMembers(
  PTP_CLEANUP_GROUP ptpcg,      
//  组清理指针
  BOOL bCancelPendingCallbacks,  //  是否取消即将开始执行的回调函数
  PVOID pvCleanupContext);       //  传入CleanupGroupCancelCallback的参数

 

  这个函数同以前的WaitForThreadpool*函数类似,当一个线程呼叫它时,它等待,直到所有的在线程工作组的项完成处理。如果第2个参数为TRUE,会取消所有还没有开始执行的项目,然后等待当前正在执行的项目,直到它们执行完成后该函数返回。如果第2个参数为TRUE,而且SetThreadpoolCallbackCleanupGroup的最后一个参数pfng传递了一个回调函数指针,那么这个回调函数就会为每个被取消的项目调用一次,CloseThreadpoolCleanupGroupMembers函数的第3个参数pvCleanupContext就会传入回调函数的第2个参数pvCleanupContext。

 

  如果在呼叫CloseThreadpoolCleanupGroupMembers函数的时候,传递FLASE给第2个参数,那么该函数就会等待线程池中队列中的所有的项完成之后才返回,此时回调函数不会被调用,因此可以传递NULL给pvCleanupContext参数。

 

  当ClsetThreadpoolCleanupGroupMembers函数返回之后,你需要呼叫CloseThreadpoolCleanupGroup来关闭一个线程池清理组:

VOID WINAPI CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);

 

  最后,要调用DestroyThreadpoolEvironment和CloseThreadpool函数来清除回调函数环境结构和关闭线程池。

 

小结

  通过前面的叙述,线程池的操作流程是比较固定的:

1、定义异步回调函数

2、创建相关项

3、提交或设置项

4、线程池执行

5、关闭相关项

 

  自己写了一段代码,总结了前面的知识,代码中省略了第1步,即没有定义回调函数,因为这是根据需要而编写的。如果代码中有错误,还请大家指出。

复制代码
PTP_POOL pThreadpool  =  CreateThreadpool(NULL);     //  创建线程池

//  设置线程池线程数量上下限
SetThreadpoolThreadMinimum(pThreadpool,  2 );
SetThreadpoolThreadMaximum(pThreadpool, 
10 );

//  初始化“回调函数环境”结构
TP_CALLBACK_ENVIRON tcbe;
InitializeThreadpoolEnvironment(
& tcbe);

//  将该回调函数环境结构与线程池相关联
SetThreadpoolCallbackPool( & tcbe, pThreadpool);

//  创建清理组
PTP_CLEANUP_GROUP pTpcg =  CreateThreadpoolCleanupGroup();

//  将回调函数环境结构与清理组关联起来
SetThreadpoolCallbackCleanupGroup( & tcbe, pTpcg, NULL);

//  现在可以创建一些项,提交给线程池
PTP_WORK pTpWork 
=  CreateThreadpoolWork( & tcbe); //  创建一个工作项
SubmitThreadpoolWork(pTpWork);     //  提交工作项
PTP_TIMER pTpTimer  =  CreateThreadpoolTimer( & tcbe); //  创建一个定时器项
SetThreadpoolTimer(pTpTimer, );       //  提交定时器
PTP_WAIT pTpWait  =  CreateThreadpoolWait( & tcbe); //  创建一个等待项
SetThreadpoolWait(pTpWait, );     //  提交等待项
PTP_IO pTpIO  =  CreateThreadpoolIo( & tcbe);     //  创建一个IO项
StartThreadpoolIo(pTpIO);     //  开始执行IO项

//  等待所有项完成
CloseThreadpoolCleanupGroupMembers(pTpcg, FALSE, NULL);

//  关闭各个项
CloseThreadpoolWork(pTpWork);
CloseThreadpoolTimer(pTpTimer);
CloseThreadpoolWait(pTpWait);
CloseThreadpoolIo(pTpIO);

CloseThreadpoolCleanupGroup(pTpcg);    
//  关闭线程池清理组
DestroyThreadpoolEnvironment( & tcbe);     //  删除回调函数环境结构
CloseThreadpool(pThreadpool);     //  关闭线程池
复制代码





《Windows via C/C++》学习笔记 —— 纤程(Fiber)

 纤程(Fiber),是微软加入到Windows中,使得UNIX服务器应用程序更好地移植到Windows中。所以本篇真正没有多少应用价值,只是为了使得笔记更加完整。

 

  看完本章,感觉纤程是比线程的更小的一个运行单位。可以把一个线程拆分成多个纤程,然后通过人工转换纤程,从而让各个纤程工作。

  要知道的是人工的转换,不是系统自动切换。因为线程的实现通过Windows内核完成的,因此Windows可以自动对线程进行调度。但是纤程是通过用户模式的代码来实现的,是程序员自己写的算法,内核不知道纤程的实现方式,而是你自己定义的调度算法,因此纤程是“非抢占”的调度方式。

  还有要知道就是,一个线程可以包含多个纤程。

 

  要使用纤程,首先要做的就是把当前线程转换为纤程:

PVOID ConvertThreadToFiber(PVOID pvParam);

 

  调用这个函数之后,系统为纤程执行环境分配大概200字节的存储空间,这个执行环境有以下内容构成:

1、用户定义的值,由参数pvParam参数指定。

2、结构化异常处理链头。

3、纤程内存栈的最高和最低地址,当线程转换为纤程的时候,这也是线程的内存栈。

4、各种CPU寄存器信息,比如堆栈指针寄存器,指令指针寄存器等等。

 

  默认情况下,x86系统的CPU的浮点数状态信息在纤程看来不属于CPU寄存器,因此会导致在纤程中执行一些相关的浮点运算会破坏数据。为了克服这个缺点,你需要呼叫ConvertThreadToFiberEx函数(Windows Vista及其以上版本中才有),并且传递FIBER_FLAG_FLOAT_SWITCH给它的第2个参数dwFlags:

PVOID ConvertThreadToFiberEx(
   PVOID pvParam,
   DWORD dwFlags);

 

  当呼叫完上述两个函数之后,你就初始化了一个纤程执行环境,该执行环境与线程的执行环境关联,线程转换为纤程,纤程就在线程的内部运行。ConvertThreadToFiber(Ex)函数实际返回纤程的执行环境的内存地址,你稍后会用到这个地址,但是你不能直接读取或写入这个地址,你应该使用系统提供的纤程函数来对这个地址进行操纵。

  当你的纤程返回或者呼叫ExitThread的时候,你的纤程也随之结束。

 

  如果一个线程中只有一个纤程,那么是没有必要将该线程转换为纤程的,只有你打算在同一个线程中再创建一个纤程才有转换的必要。要创建一个纤程,使用CreateFiber函数:

PVOID CreateFiber(
   DWORD dwStackSize,     
//  创建新的堆栈的大小,0表示默认大小
   PFIBER_START_ROUTINE pfnStartAddress,      //  纤程函数地址
   PVOID pvParam);      //  传递给纤程函数的参数

 

  这个函数创建一个新的堆栈,堆栈的大小由dwStackSize指定。如果传递0给它,就意味着创建一个默认大小的堆栈。

  如果你打算让一个线程包含多个纤程,而又想花费比较少的空间的话,可以使用CreateFiberEx函数(只有在Windows Vista及其以上版本中才有):

复制代码
PVOID CreateFiberEx(
   SIZE_T dwStackCommitSize,     
//  堆栈初始提交的大小
   SIZE_T dwStackReserveSize,     //  需要保留的虚拟内存的大小
   DWORD dwFlags,      //  创建旗标
   PFIBER_START_ROUTINE pStartAddress,      //  纤程函数指针
   PVOID pvParam);      //  传递给纤程函数的参数
复制代码

 

  其中,如果传递FIBER_FLAG_FLOAT_SWITCH给dwFlags参数,则表明将浮点信息添加到纤程执行环境。

 

  当CreateFiber(Ex)函数创建了一个新的堆栈之后,它分配一个新的纤程执行环境结构并初始化之,用户定义的数据通过pvParam参数被保存,新的堆栈的内存空间的最高和最低地址被保存,纤程函数的地址通过pStartAddress参数被保存。

  纤程函数的格式必须如下定义:

VOID WINAPI FiberFunc(PVOID pvParam);

  这个纤程在第一次被调度的时候,纤程函数被调用,其参数pvParam由CreateFiber(Ex)中的pvParam参数指定。在纤程函数中,你可以做你想做的任何事情。

  像ConvertThreadToFiber(Ex)函数一样,CreateFiber(Ex)也返回纤程执行环境的内存地址,这个内存地址就像句柄一样,直接标识着一个纤程。

  当你使用CreateFiber(Ex)函数创建一个纤程之后,该纤程不会执行,因为系统不会自动调度它。你必须调用函数SwitchToFiber来告诉系统你想要哪个纤程执行:

VOID SwitchToFiber(PVOID pvFiberExecutionContext);

 

  SwitchToFiber函数的参数是一个纤程执行环境的内存地址,该地址由ConverThreadToFiber(Ex)或CreateFiber(Ex)返回。

  SwitchToFiber函数内部的执行步骤如下:

1、保存当前的CPU寄存器信息,这些信息保存在正在运行的纤程的执行环境中。

2、从将要执行的纤程的执行环境中加载上次保存的CPU寄存器信息。

3、将即将执行的纤程执行环境与线程关联起来,由线程执行指定的纤程。

4、将指令指针设置为保存的值,继续上次的执行。

 

  SwitchToFiber函数是一个纤程能够被调度的唯一的方法,因此,纤程的调度是由用户完全操纵的。纤程的调度和线程的调度无关。一个线程,包含了正在运行的纤程,仍会被其他线程抢占。当一个线程被调度,而它里面有几个纤程,那么只有被选择的那个纤程才会执行,其他纤程的执行需要调用SwitchToFiber函数。

 

  最后,如果一个纤程完成了任务,你需要删除它,呼叫DeleteFiber函数,并传递这个纤程的执行环境内存地址:

VOID DeleteFiber(PVOID pvFiberExecutionContext);

 

   该函数首先清除纤程堆栈,然后删除纤程执行环境。但是,如果参数指定的是一个与当前线程关联的纤程,该函数呼叫ExitThread函数,线程结束,其包含的其他纤程也都结束。因此,DeleteFiber函数一般是由一个纤程调用来删除另一个纤程。

  当所有纤程结束了运行,你需要从纤程转换为线程,呼叫ConvertFiberToThread函数。

 

  如果你需要在纤程中保存一些数据,可以使用“纤程局部存储”(FLS)的机制。这个机制和“线程局部存储”(TLS)类似。

  首先,呼叫FlsAlloc函数分配FLS槽来存放数据,这个FLS槽可以被当前进程内所有纤程共同使用,函数有一个参数:一个回调函数指针,这个回调函数会在以下两种情况下被调用:一个纤程被删除;FLS槽通过FlsFree函数被删除。

  然后,在你呼叫FlsAlloc函数之后,你可以在纤程中使用FlsSetValue函数来保存数据到FLS槽中,同时该函数需要一个DWORD类型的参数,表示一个FLS槽的索引,即在FLS槽的相关地方保存数据。

  接着,你可以在各个纤程中使用FlsGetValue函数来取得FLS槽中对应的数据,同样需要上面那个FLS槽索引,并返回指向数据的指针。

  当使用完这些数据之后,你可以使用FlsFree来释放FLS槽。

 

  如果你想知道你是否正在一个纤程执行环境中运行,可以使用IsThreadAFiber函数,它返回一个BOOL值,指明你是否正在一个纤程中运行。

 

  一个线程每次只能执行一个纤程,该纤程与这个线程相关联。你可以使用如下函数来得到正在执行的纤程的执行环境内存地址:

PVOID GetCurrentFiber();

 

  每个纤程包含用户定义的一个数据,这个数据由CreateFiber(Ex)或ConvertThreadToFiber(Ex)的pvParam参数指定,你可以使用如下函数得到这个数据的指针:

PVOID GetFiberData();

 

  最后,让我们假设一个线程中有2个纤程,总结一下纤程的用法:

1、使用ConverThreadToFiber(Ex)将当前线程转换到纤程,这是纤程F1

2、定义一个纤程函数,用于创建一个新纤程

3、纤程F1中调用CreateFiber(Ex)函数创建一个新的纤程F2

4、SwitchToFiber函数进行纤程切换,让新创建的纤程F2执行

5、F2纤程函数执行完毕的时候,使用SwitchToFiber转换到F1

6、在纤程F1中调用DeleteFiber来删除纤程F2

7、纤程F1中调用ConverFiberToThread,转换为线程

8、线程结束




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值