zoukankan      html  css  js  c++  java
  • 《Windows核心编程》学习笔记(11)– Windows线程池

    以异步方式调用函数

    为了用线程池来以异步的方式执行一个函数,我们需要定义一个具有以下原型的函数:

    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结构的指针,这个指针式我们在调用ReadFileWriteFile来发出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
    );

     

  • 相关阅读:
    201871010132张潇潇《面向对象程序设计(java)》第六七周学习总结
    201871010132张潇潇《面向对象程序设计(java)》第七周总结
    201871010132张潇潇《面向对象程序设计(java)》第十周总结
    201871010132张潇潇《面向对象程序设计(java)》第四周学习总结
    201871010132张潇潇《面向对象程序设计(java)》第十三周学习总结
    201871010132张潇潇《面向对象程序设计(java)》第一周学习总结
    201871010132张潇潇《面向对象程序设计(java)》第八周总结
    201871010132——张潇潇《面向对象程序设计JAVA》第二周学习总结
    201871010132张潇潇《面向对象程序设计(Java)》第十一周学习总结
    201871010132张潇潇《面向对象程序设计(java)》第十二周学习总结
  • 原文地址:https://www.cnblogs.com/forlina/p/2133450.html
Copyright © 2011-2022 走看看