[笔记]Windows核心编程《十一》Windows线程池

简介: [笔记]Windows核心编程《十一》Windows线程池

前言

线程池通常含义指 一个固定数量的线程队列。每当需要一个线程去执行某任务(某段代码),从队列中选出一个闲置的线程去执行,当线程执行完某任务后,不会立即销毁,会回到队列中,等待执行其他任务。

为了简化程序员的工作,Windows提供了一个线程池机制来简化线程的创建、销毁以及日常管理。这个新线程池可能不适用于所有的情况,但大多数情况下它都能够满足我们的需要。

这个线程池能够帮助我们做一下事情:

  1. 以异步的方式调用一个函数。
  2. 每隔一段时间调用一个函数。
  3. 当内核对象触发时调用一个函数。
  4. 当异步IO请求完成时调用一个函数。

11.1 情形1:以异步的方式调用函数

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

VOID NTAPI SimpleCallback(
PTP_CALLBACK_INSTANCE pInstance,
PVOID pvContext
);

为了让线程池中的一个线程执行该函数,我们需要向线程池提交一个请求。

BOOL TrySubmitThreadpoolCallback(  
      PTP_SIMPLE_CALLBACK pfnCallback,  
      PVOID pvContext,  
      PTP_CALLBACK_ENVIRON pche);  

若调用成功,则返回true。否则返回false。

系统会自动为我们的进程创建一个默认的线程池,并让线程池中的一个线程来调用我们的回调函数。当这个线程处理完一个客户请求后,不会立即被销毁,而是会回到线程池,准备好处理队列中的其它工作项。线程池会不断重复使用其中的线程。如果线程池检测到创建另一个线程将能够更好地为应用程序服务,那它就会这样做。如线程池检测到它的线程数量已供过于求,那么它就会销毁其中一些。

11.1.1 显式地控制控制

在某些情况下,如内存不足时TrySubmitThreadpoolCallback可能会失败。每一次调用TrySubmitThreadpoolCallback时,系统会在内部分配一个工作项。

如果打算提交大量的工作项,出于性能和内存使用方面的考虑,应该手动创建工作项然后多次提交它。

创建一个工作项

VOID CALLBACK WorkCallback(  
        PTP_CALLBACK_INSTANCE Instance,  
        PVOID Context,  
        PTP_WORK Work);
PTP_WORK CreateThreadpoolWork(
    PTP_WORK_CALLBACK pfnWorkHandler,
    PVOID pvContext,
    PTP_CALLBACK_ENVIRON pcbe
);

该函数会在用户模式内存中创建一个结构来保存它的三个参数,并返回指向该结构的指针。

pfnWorkHandler :函数指针,线程池中的线程最终对工作项处理时,会调用该函数指针指向的函数。

pvContext :传给回调函数的值。

pcbe : 如果传给它NULL则表示我们会将工作项添加到默认的线程池中。一般情况下默认的线程池能够满足大多数情况下的要求。

向线程池提交一个请求

VOID SubmitThreadpoolWork(PTP_WORK pWork); 

取消已经提交的工作项或是等待工作项处理完毕

如果我们想取消已经提交的工作项或是等待工作项处理完毕。可以调用以下函数:

VOID WaitForThreadpoolWorkCallbacks(  
      PTP_WORK pWork,  
      BOOL bCancelPendingCallbacks);  

此函数将线程挂起,直到工作项处理完毕。

pWork : 指向一个工作项。此工作项可以是CreateThreadpoolWork和SubmitThreadpoolWork来创建和提交的。如果工作项尚未被提交,那么等待函数立即返回。

bCancelPendingCallbacks

TRUE,当指定的工作项尚未被处理,函数将其标记为取消并返回;当指定的工作项正在被处理,等待处理完成后,再返回;当同一工作项被提交多次时,只等待当前正处理的。

FALSE,当指定的工作项被处理完且处理它的线程已经被收回并准备处理下一个工作项时,才返回;当同一工作项被提交多次时,等待所有均被处理后,返回。

关闭工作项

当不需要一个工作项时,可以调用CloseThreadpoolWork,并传入指向该工作项的指针。

VOID CloseThreadpoolWork(PTP_WORK pwk);  

11.1.2 Batch示例程序

Batch示例程序,如何使用线程池的工作项。

待。

11.2 情形2:每隔一段时间调用一个函数

有时候应用程序需要在某些时间执行某些任务。Windows提供了可等待计时器对象,它使我们我们可以非常方便的得到一个时间通知。我们可以为每个需要执行基于时间的任务创建一个可等待的计时器对象,但这是不必要的。线程池函数为我们解决了这些事情。

11.2.1 线程池实现计时器

通知线程池何时调用我们回调

为了将一个工作项安排在某个时间执行,我们必须定义一个回调函数。该函数会在某个时刻被调用。回调函数原型为:

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);  

这个函数与前面介绍的CreateThreadpoolWork相似。CreateThreadpoolTimer返回计时器对象。该计时器对象由CreateThreadpoolTimer函数创建并返回。

pfnTimerCallback :是一个函数指针。指向前面介绍的回调函数TimeroutCallback。每当线程池调用pfnTimerCallback指向的函数时会将pvContext传给它,并传给pTimer一个由CreateThreadpoolTimer返回的计时器对象指针。

pvContext:为传给回调函数参数。

pcbe:使用的线程池类型 同上,为NULL则是默认线程池。

向线程池注册计时器(或者对计时器进行修改)

当我们想要向线程池注册计时器时,应该调用SetThreadpoolTimer:

VOID SetThreadpoolTimer(  
       PTP_TIMER pTimer,  
       PFILETIME pftDueTime,  
       DWORD msPeriod,  
       DWORD msWindowLength);  

pTimer:用来标识CreateThreadpoolTimer返回的计时器对象。

pftDueTime:表示第一次调用回调函数是什么时候。传入一个负值表示一个相对时间。该时间相对于调用SetThreadpoolTimer的时间。传入-1表示立即调用。传入的正值以100ns为单位,从1600年的1月1日开始计算。

msPeriod:表示调用回调函数的时间间隔,传入0表示只调用1次。

msWindowLength:用来给回调函数的执行增加一些随机性。这使得回调函数会在当前设定的时间到当前设定的触发时间加上msWindowLength设定的时间之间触发。这对于多个计时器来说非常有用。这可以避免多个计时器间的冲突。

确定某个计时器是否已经被设置

BOOL IsThreadpoolTimerSet(PTP_TIMER pti); 

等待一个计时器完成

最后我们可以通过WaitForThreadpoolTimerCallbacks来等待一个计时器完成。调用CloseThreadpoolTimer来释放计时器的内存。它们与前面介绍的WaitForThreadpoolWork和CloseThreadpoolWork相似。

11.2.2 Timed Message Box示例程序

待完成。

11.3 情形3:在内核对象触发时调用一个函数

在实际使用中我们会发现我们会经常的等待一个内核对象被触发,触发后等待线程又会进入下一轮循环继续等待。Windows线程池提供了一些机制可以简化我们的工作。

内核对象被触发时执行某函数

如果我们想让内核对象被触发时执行某函数。需要进行以下步骤,

首先编写一个回调函数,它是内核对象被触发时被调用的函数。需要满足以下原型:

VOID CALLBACK WaitCallback(
    PTP_CALLBACK_INSTANCE pInstance,
    PVOID Context,
    PTP_WAIT Wait,
    TP_WAIT_RESULT WaitResult);

绑定内核对象到线程池

然后创建CreateThreadpoolWait来将一个内核对象绑定到线程池:

VOID SetThreadpoolWait(
   PTP_WAIT pWaitItem,
   HANDLE hObject,
   PFILETIME pftTimeout);

pWaitItem:用来标识CreateTheadpoolWait返回的对象。

hObject:用来标识内核对象。当此对象被触发时,回调函数会被调用。

pftTimeout:用来表示线程池最长应该花多少时间来等待内核对象被触发。传入0表示不用等待。传入负值表示相对时间。传NULL表示无限长的时间。

获得WaitCallback被调用的原因

线程池内部会让一个线程调用WaitForMultipleOBjecs函数。传入SetThreadpoolWait函数注册的一组句柄,并传入false给bWaitAll参数。当任何一个内核对象被触发时,线程池就会被唤醒。当内核对象被触发或是超出等待时间时,线程池的某个线程就会调用我们的回调函数(WaitCallback)。

WaitResult :用来表示WaitCallback被调用的原因。它可以是以下值:

  • WAIT_OBJECT_0 超时之前有对象被触发。
  • WAIT_TIMEOUT 由于超时导致回调函数被触发。
  • WAIT_ABANDONED_0 如果传入的内核对象是互斥量且被遗弃。回调函数将收到这个值。

一旦线程池调用了我们的回调函数,对应的等待项将进入不活跃状态。所谓不活跃状态:如果想让回调函数在同一个内核对象被触发时再次被调用,我们需要调用SetThreadpoolWait来再次注册。

等待一个等待项完成

最后我们同样可以等待一个等待项完成。这可以调用:

WaitForThreadpoolWaitCallbacks。

释放一个等待项的内存

CloseThreadpoolWait

注意:不要让回调函数调用WaitForThreadpoolWork并将自己的工作项作为参数传入,这会导致死锁。

11.4 情形4: 在异步I/O 请求完成时调用一个函数

我们在上一篇博文中介绍了如何使用IO完成端口来高效的执行异步IO操作,也介绍了如何创建一个线程池并让其中的线程等待IO完成端口。

这里我们将介绍线程池如何管理线程的创建和销毁。在打开一个关联起来文件或设备时,我们必须现将该设备与线程池的IO完成端口,然后告诉线程池在异步IO完成时应该调用哪个函数。

定义回调函数

首先我们需要定义回调函数,它需要满足一下原型

VOID CALLBACK OverlappedCompletionRoutine(  
       PTP_CALLBACK_INSTANCE pInstance,  
       PVOID pvContext,  
       PVOID pOverlapped,  
       ULONG IoResult;  
       ULONG_PTR NumberOfBytesTransferred,  
       PTP_IO  pIo);

当一个IO操作完成时此回调函数会被调用并得到一个指向OVERLAPPED结构的指针。这个指针是我们在调用ReadFile或WriteFile来发出I/O请求时(通过pOverlapped参数)传入的。

IoResult:表示IO异步操作的执行结果。如果IO请求成功,将传给回调函数NO_ERROR。

NumberOfBytesTransferred :参数传入已传输的字节数。

pIo :传入指向线程池IO项的指针。马上介绍。

pInstance :后面会有介绍。

创建一个线程池IO对象

定义好回调函数后,我们就需要调用CreateThreadpoolIo来创建一个线程池IO对象。

PTP_IO CreateThreadpoolIo(  
       HANDLE hDevice,  
       PTP_WIN32_IO_CALLBACK pfnIoCallback,  
       PVOID pvContext,  
       PTP_CALLBACK_ENVIRON pcbe);  

hDevice :是与IO对象相关联的设备句柄。

pfnIoCallback :是前面我们介绍的回调函数指针。

pvContext : 当然是传给回调函数的参数。

IO项中的设备与IO完成端口关联

当IO对象创建好之后,我们就可以通过下面的函数来将嵌入在IO项中的设备与IO完成端口关联起来。

VOID StartThreadpoolIo(PTP_IO pio);  

关联之后我们就可以调用ReadFile或WriteFile了。此后当异步IO请求完成后,回调函数将会被调用。

注意,在每次调用ReadFile或WriteFile之前,我们必须调用StartThreadpoolIo,否则回调函数不会被调用。

停止线程池调用回调函数

此外我们还可以调用以下函数来停止线程池调用回调函数,此后回调函数将不会被调用:

VOID CancelThreadpoolIo(PTP_IO pio);  

取消设备与线程池的关联

CloseThreadpoolIo将取消设备与线程池的关联:

VOID CloseHandleoolIo(PTP_IO pio);

让另一个线程等待一个待处理的IO请求完成

WaitForThreadpoolIoCallbacks函数让另一个线程等待一个待处理的IO请求完成。

VOID WaitForThreadpoolIoCallback(  
      PTP_IO pio,  
      BOOL bCancelPendingCallbacks);  

如果传给bCancelPendingCallbacks的值为true,那么当请求完成时,回调函数不会被调用。

11.5 回调函数的终止操作

线程池提供了一种便利的方法,来描述我们的回调函数返回后,应该执行的一些操作。回调函数用传给它的不透明的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
);

11.5.1 对线程池进行定制

在调用CreateThreadpoolWork,CreateThreadpoolTimer,CreateThreadpoolWait或CreateThreadpoolIo时,有机会传入一个PTP_CALLBACK_ENVIRON参数。如传NULL,会将工作项添加到进程默认的线程池中。

创建新的线程池

PTP_POOL CreateThreadpool(
// 保留,传NULL。
PVOID reserved
);
// 设置线程池中线程最大,最小数量
BOOL SetThreadpoolThreadMinimum(
PTP_POOL pThreadPool,
DWORD cthrdMin
);
BOOL SetThreadpoolThreadMaximum(
PTP_POOL pThreadPool,
DWORD cthrdMost
);
// 关闭线程池。
// 线程池中线程结束本次处理后结束。
// 线程池队列中尚未开始处理的项将被取消。
VOID CloseThreadpool(PTP_POOL pThreadPool);

线程池始终保持池中的线程数量至少是最小数量,并允许线程数量增长到指定的最大数量。

回调环境pcbe

一旦我们创建了自己的线程池,并指定了线程的最小数量和最大数量,我们就可初始化一个回调环境,它包含了一些可用于工作项的额外的设置或配置。

线程池回调环境数据结构:

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
);
// 清理
VOID DestroyThreadpoolEnvironment(
PTP_CALLBACK_ENVIRON pcbe
);
// 为了将一个工作项添加到线程池的队列中,回调环境必须标明该工作项应由那个线程池来处理。不调用时,pcbe初始化后的Pool字段为NULL,添加到进程默认的线程池。
VOID SetThreadpoolCallbackPool(
PTP_CALLBACK_ENVIRON pcbe,
PTP_POOL pThreadPool
);
// 确保,只要线程池中还有待处理的工作项,就将一个特定的DLL一直保持在进程的地址空间中。
VOID SetThreadpoolCallbackLibrary(
PTP_CALLBACK_ENVIRON pcbe,
PVOID mod
);
.

11.5.2 得体地销毁线程池:清理组

线程池可处理大量的队列项,这些项的来源各不相同。

这使我们很难知道线程池结束处理队列项的确切时间,但只有这样才能得体地将它销毁。

为了帮助我们对线程池进行得体的清理,线程池提供了清理组。

默认的线程池不会被销毁。在进程终止时,windows会将其销毁并负责所有的清理工作。

创建一个清理组

PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();

将清理组与一个已经绑定到线程池的TP_CALLBACK_ENVIRON结构关联

VOID SetThreadpoolCallbackCleanupGroup(
  PTP_CALLBACK_ENVIRON pcbe,
  PTP_CLEANUP_GROUP ptpcg,
  // 清理组被取消时,回调函数会被调用
  PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);
VOID CALLBACK CleanupGroupCancelCallback(
  PVOID pvObjectContext,
  PVOID pvCleanupContext
);

这个函数在内部会设置PTP_CALLBACK_ENVIRON的CleanupGroup字段和CleanupGroupCancelCallback字段。

当调用CreateThreadpoolWork,CreateThreadpoolTimer,CreateThreadpoolWait,CreateThreadpoolIo时,如最后的的参数,即指向PTP_CALLBACK_ENVIRON结构的指针不为NULL,那么创建的项会被添加到对应的回调环境的清理组中。

在这些队列项完成后,如我们调用了CloseThreadpoolWork,CloseThreadpoolTimer,CloseThreadpoolWait和CloseThreadpoolIo,等于是隐式地将对应的项从清理组中移除。

销毁线程池

// 函数会一直等待,直到线程池的工作组中所有剩余的项(创建但未关闭的)都已处理完毕。
VOID CloseThreadpoolCleanupGroupMembers(
PTP_CLEANUP_GROUP ptpcg,
// TRUE,在所有当前正在运行的工作项完成后返回。尚未处理的取消。
// FALSE,等待所有项处理完毕
BOOL bCancelPendingCallbacks,
PVOID pvCleanupContext
);

如果传给bCancelPendingCallbacks值为TRUE,且传给SetThreadpoolCallbackCleanupGroup的pfng不为NULL。则,对每个被取消的工作项,pfng会被调用。回调函数的pvObjectContext包含每个被取消项的上下文(CreateThreadpoolxxx时设置的)。回调函数的pvCleanupContext是上述调用的最后一个参数。

释放清理组

VOID WINAPI CloseThreadpoolCleanupGroup(
PTP_CLEANUP_GROUP ptpcg
);
// 销毁自定义线程池的环境
DestroyThreadpoolEnvironment
// 关闭线程池
CloseThreadpool

总结

相关:

参考1

参考2

参考3


相关文章
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
183 1
|
6月前
|
Web App开发 人工智能 JSON
Windows版来啦!Qwen3+MCPs,用AI自动发布小红书图文/视频笔记!
上一篇用 Qwen3+MCPs实现AI自动发小红书的最佳实践 有超多小伙伴关注,同时也排队在蹲Windows版本的教程。
1078 1
|
监控 Ubuntu Linux
视频监控笔记(五):Ubuntu和windows时区同步问题-your clock is behind
这篇文章介绍了如何在Ubuntu和Windows系统中通过设置相同的时区并使用ntp服务来解决时间同步问题。
275 4
视频监控笔记(五):Ubuntu和windows时区同步问题-your clock is behind
|
网络协议 API Windows
MASM32编程调用 API函数RtlIpv6AddressToString,windows 10 容易,Windows 7 折腾
MASM32编程调用 API函数RtlIpv6AddressToString,windows 10 容易,Windows 7 折腾
[原创]用MASM32编程获取windows类型
[原创]用MASM32编程获取windows类型
|
JavaScript 前端开发 API
MASM32编程通过WMI获取Windows计划任务
MASM32编程通过WMI获取Windows计划任务
|
设计模式 缓存 Java
谷粒商城笔记+踩坑(14)——异步和线程池
初始化线程的4种方式、线程池详解、异步编排 CompletableFuture
谷粒商城笔记+踩坑(14)——异步和线程池
|
API Windows
MASM32编程获取Windows当前桌面主题名
MASM32编程获取Windows当前桌面主题名
|
数据库 Windows
超详细步骤解析:从零开始,手把手教你使用 Visual Studio 打造你的第一个 Windows Forms 应用程序,菜鸟也能轻松上手的编程入门指南来了!
【8月更文挑战第31天】创建你的第一个Windows Forms (WinForms) 应用程序是一个激动人心的过程,尤其适合编程新手。本指南将带你逐步完成一个简单WinForms 应用的开发。首先,在Visual Studio 中创建一个“Windows Forms App (.NET)”项目,命名为“我的第一个WinForms 应用”。接着,在空白窗体中添加一个按钮和一个标签控件,并设置按钮文本为“点击我”。然后,为按钮添加点击事件处理程序`button1_Click`,实现点击按钮后更新标签文本为“你好,你刚刚点击了按钮!”。
1407 0
|
Windows 数据安全/隐私保护 网络协议