回炉重造之重读Windows核心编程-011-线程池和其他异步方式

线程池的使用

多线程应用程序很难设计,有两大难点,一是要管理线程的创建和撤销,再就是要对线程访问资源时实施同步。同步的工具有几个了。为了应对线程频繁地创建和撤销,线程池这个方案被放上了台面。Windows2000提供了一些新的线程池函数,使得线程的创建、撤销和基本管理更加容易。线程池的实现不拘一格,只要遵循以下的要点:

  1. 异步调用函数。
  2. 按照规定的时间间隔调用函数。
  3. 当单个内核对象变为已通知状态时调用函数。
  4. 当异步IO请求完成时调用函数。

为了完成这些操作,线程池由4个独立的部分组成。下面shixianc


线程初始值


创建一个线程时


线程被撤销时


线程如何等待


是什么唤醒了线程


定时器


总是1


当调用一个函数时


进程终止运行


待命状态


定时器通知排队的用户APC


等待


0


每63个注册对象有一个线程


注册的等待对象数量是0


WaitForMultiple-

Objects


内核对象变为已通知状态


I/O


0


系统使用试探算法,也有影响线程创建的因素:

  1. 添加线程后已经过去一段时间
  2. 使用WT_EXECUTELONGFU

NCTION标志

  1. 排队的工作项目超过了某个阈值

线程没有未处理的I/O请求并且已经等待了一个阈值周期时


线程没有未处理的I/O请求并且已经等待了一个阈值周期时


排队的用户API和已完成的I/O请求


非I/O


0


当线程空闲了一个周期时


GetQueuedCom-

plectionStatus


展示已完成的状态和I/O请示(完成端口最多允许数量为2*CPU同时运行的数量)

可见一旦新线程池函数之一被调用,就为进程创建某些组件,其中一些还将被保留到进程终止运行为之。因此线程池运行的开销并不小,需要谨慎地使用线程池。

11.1线程池的方案1

新的线程池也可以应对传统的客户端/服务器架构,通常调用这个函数:

BOOL QueueUserWorkItem(

PTHREAD_START_ROUTINE pfnCallback,

PVOID   pvContext,

ULONG  dwFlags);

这个函数将一个“工作项目”放进线程池中的一个线程并立即返回。工作项目是指一个函数,它被调用并传递单个参数pvContext。最后线程池中的某个线程将处理该工作项目,这个函数的原型是:

DWORD WINAPI WorkItemFunc(PVOID pvContext);

线程池中的线程运行结束之后并不会立即撤销,而是返回线程池,准备好处理其他的工作项目,这样可以省略对线程频繁的创建和撤销,以节省更多的开销,提高运行的效率;由于和完成端口相关联,可以同时运行的线程数量是CPU数量的两倍,还节省了线程上下文转移的开销。

这个函数的内部情况是:

  1. QueueUserWorkItem检查非I/O组件中的线程数量,然后根据负荷量(已经排队的工作项目的数量)将另一个线程添加给组件。
  2. QueueUserWorkItem执行对PostQueuedCompletionStatus的等价调用,把工作项目的信息传递给I/O端口。
  3. 在完成端口上等待的线程调用GetQueuedCompletionStatus取出信息,并调用函数。
  4. 当函数返回时,该线程继续调用GetQueuedCompletionStatus等待另一个工作项目。

异步IO是创建高性能可伸缩的应用程序的秘诀,但是Windows对异步IO请求规定了一个限制:如果线程发出一个异步IO请求,而线程池因为缩小而撤销,那么这个IO请求也会撤销。因此发出异步请求的时候,一定要分清楚IO和非IO的,当用QueueUserWorkItem函数的时候,给dwFlags参数传递WT_EXECUTEINIOTHREAD。如果只传递WT_EXECUTEDEFAULT,工作项目就会防护非IO组件的线程中。

Microsoft还提供了函数(如RegNotifykeyValue)能够异步执行与非IO相关的任务。这些线程也要求带哦用线程不能终止运行。使用WT_EXECUTEINPERSISTENTTHREAD标志,可以调用永久线程池中的一个,它使得定时器组件的线程能够执行已排队的工作项目函数。由于定时器组件的线程不会停止运行,因此可以确保最终发生异步操作,保证回调函数不会中断而是迅速执行。

现在的矛盾是,线程池需要是设计成可以随时处理工作项目的,但是又不能确定工作项目将要花费多长的时间。当然可以为QueueUserWorkItem传递WT_EXECUTELONGFUNCTION,这个标志可以帮忙觉得是否要将新线程添加给线程池:

线程池不能对线程池的线程数量规定一个上限,否则就会发生死锁现象,所以要寻找潜在的死锁条件,即使是使用了关键代码段、信标和互斥对象上中断运行。始终应该了解是那个组件(IO、非IO、等待定时器等)的线程正常运行你的代码。

还有DLL。如果工作项目的函数位于已经被动态卸载的DLL中,那么就有违规访问的风险。这种情况必须要在调用QueueUserWorkItem时递增引用计数的值,当引用计数变为0的时候,才是卸载DLL的时机。

11.2方案2:按规定的时间间隔调用函数

如果为了一个基于时间的任务创建一个等待定时器对象,是不必要的,浪费系统资源。而创建一个等待定时器,为它设定下一个预定运行的时间,然后为下一个时间重置定时器。再用线程池函数管理就方便多了。

首先是创建一个定时器队列 :

HANDLE CreateTimerQueue();

这样就有了一组安排定时器的定时器队列。一旦拥有它,就可以在改队列中创建定时器。

BOOL CreateTimerQueueTimer (

PHANDLE phNewTimer,

HANDLE hTimerQueue,

WAITOFTIMERCALLBACK pfnCallback,

PVOID pvContext

DWORD dwDueTime,

DWORD dwPeriod,

ULONG dwFlags);

第二个参数是定时器队列的句柄,如果定时器数量少,传递NULL即可,还不用调用CreateTimerQueue,使用默认的定时器队列,简化代码;pfnCallback和pvContext分别是函数和返回值;dwDueTime表示第一次调用函数的时间(如果是0,那就尽可能地调用函数,那么CreateTimerQueueTimer就像QueueUserWorkItem了);dwPeriod是再次执行函数的间隔时间,如果是0,排一次队就结束。pfnCallback的原型如下:

VOID WINAPI WaitOrTimerCallback(

PVOID pvContext,

BOOL fTimerOrWaitFired);

函数被调用的时候fTimerOrWaitFired总是TRUE,表示定时器已经触发。

dwFlags参数告诉函数如何给工作项目排队:

  1. 如果是非IO组件的线程来处理工作项目,则使用WT_EXECUTEDEFALTE。
  2. 如果是让一个不会终止的工作项目来处理,则使用WT_EXECUTEPERSISTENTTHREAD。
  3. 如果是执行长时间的函数,使用WT_EXECUTELONGFUNCTION。
  4. 如果是WT_EXECUTEINTIMERTHREAD,可以使得定时器组件的线程能够更高效地运行,但也很危险,有可能无法执行其他的任务。即使等待定时器会将APC项目放入该线程,在这个函数返回前,其他函数是不能得到处理的。

WT_EXECUTEINTOTHREAD、WT_ EXECUTEPERSISTENTTHREAD和EXECUTEINTIMERTHREAD等标志是互斥的,如果不传递它们之中的任何一个,工作项目就放入IO组件的线程中。

删除定时器的方式:

BOOL DeleteTimerQueueTimer(

HANDLE hTimerQueue,

HANDLE hTimer,

HANDLE hCompletionEvent);

hCompletionEvent通知你,什么时候不在存在没有处理的已经排队的工作项目,如果是INVALID_HANDLE_VALUE,那么在所有已经排队的工作项目完成运行前,函数DeleteTimerQueueTimer不会返回。这是有风险的,如果有死锁的话。

删除一个正在执行的定时器也会产生死锁。试图删除定时器的话,会将一个APC通知放出定时器组件的线程队列中,而这个线程正在等待一个定时器被删除,却删除不掉,就会产生死锁。不传递INVALID_HANLDE_VALUE,而传递NULL,这是告诉函数你想尽快删除定时器。这样函数就立即返回,但是你不知道该定时器的项目何时被完成处理。

最稳妥的做法是传递一个时间内核对象的句柄,这样DeleteTimerQueueTimer直接返回,也可以让定时器的所有工作项目完成之后,定时器组件设置这个事件。

创建一个定时器后,是可以改变它的到期时间和周期的:

BOOL ChangeTimerQueueTimer(

HANDLE hTimerQueue,

HANDLE hTimer,

ULONG dwDueTime,

ULONG dwDueTime);

可以修改dwDueTime和dwDueTime,只是修改已经触发的单步定时器不起作用。另外调用这个函数没有死锁的风险。

删除定时器队列:

BOOL DeleteTimerQueueEx (

HANDLE hTimerQueue,

HANDLE hCompletionEvent);

它取出定时器队列的句柄,并删除其中所有的定时器,不必调用DeleteTimerQueueTimer。hCompletionEvent语义相同,同样有死锁的风险。

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

其实多数的线程只是为了等待内核对象变为已通知状态,之后执行代码,再把内核对象变为未通知状态。实现的时候,有些方式是若干个线程各自等待一个内核对象,这对系统资源是相当大浪费。

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

BOOL RegisterWaitForSingleObject(

PHANDLE phNewWaitObject,

HANDLE hObject,

WAITEORTIMERCALLBACK pfnCallback,

PVOID pvContext,

ULONG dwMillisecond,

ULONG dwFlags);

当注册成功,phNewWaitObject返回一个句柄以表示这个等待组件;参数hObject告诉函数想要在那个内核对象得到通知的时候,对工作项目进行排队;函数将参数pvContext传递给pfnCallback;dwMillisecond可以是0或者INFINITE,这个函数的运行情况和WaitForSingleObject相似。

在内部,等待组件使用WaitForMultipleObjects来等待已经注册的对象,并受到该函数已经存在的任何限制,其中之一就是不能等待多个句柄。要多次注册单个对象,使用函数DuplicateHandle,对原始句柄和复制句柄分开注册。由于调用了WaitForMultipleObjects,等待的内核对象不能超过64个。每隔63个对象,就要将另一个线程添加给这个组件,因为这些线程也必须等待负责控制超时的等待定时器对象。

工作项目准备执行时,模式是放入非IO组件的线程中的。线程之一醒来后就调用你的函数,函数的原型必须是下面的形式:

VOID WINAPI WaitOrTimerCallbaFunc(

PVOID pvContext,

BOOLEAN fTimerOrWaitFired);

如果等待超时fTimerOrWaitFired就是TRUE,否则就是FALSE。上面说过如果给函数RegisterWaitForSingleObject传递WT_EXECUTEWAITTHREAD,它让线程之一的运行工作项目本身,这样效率更高,因为不必放入IO组件。由于有其他工作项目的等待函数无法得到通知,所以也是有一定的危险性。

如果工作项目将要发出异步的IO请求,或者使用从不终止的线程来执行某些操作的话,那么也可以传递WT_EXECUTEINTOTHREAD或者WT_PERSISTENTTHREAD。也可以使用WT_EXECUTELONGCUNCTION标志告诉线程池,你的函数可能需要花费较长的时间来运行,而且应该考虑讲一个新的线程添加给线程池。只有工作项目被移动到IO或者非IO组件的线程中,才能使用该标志。

最后一个标志WT_EXECUTEONLYONCE。该标志告诉等待组件在工作项目执行了一次后就停止等待该对象。如果等待一个自动重置的事件内核对象,一旦该对象变为已通知状态,它就停留在这个状态中,这会导致等待组件不断地给工作项目排队。这时使用这个标志,就可以防止这种情况的发生。

除了使用EXECUTEONLYONCE标志,调用下面的函数也可以取消等待组件的注册状态。

BOOL UnregistererWaitEx(

HANDLE hWaitHandle,

HANDLE hCompletionEvent);

第一个参数指明一个已经注册的等待组件的句柄(由函数RegisterWaitForSingleObject返回),第二个参数表示你想如何被通知。和上面一样,hCompletionEvent可以传递NULL(如果不需要通知),也可以传递INVALID_HANDLE_VALUE(中断对函数的调用,直到所有排队的项目都已经执行),也可以传递一个事件对象的句柄(当排队的工作项目已经执行就会得到通知)。如果没有中断函数的调用,函数返回TRUE,否则返回FALSE,GetLastError返回STATUS_PENDING。

同样,传递INVALID_HANDLE_VALUE有死锁的风险。在曲线等待组件的注册状态之前,不要关闭内核对象的句柄。这会令句柄无效。同时,等待组件的线程会在内部使用WaitForMu

11.4方案4:当异步IO请求完成时调用函数

这个方法是通过IO完成端口,并创建一个等待该端口的线程池。还需打开多个IO设备,将他们的句柄与完成端口关联起来,当IO请求完成时,设备驱动程序就将“工作项目”排队列入该完成端口。

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

BOOL BindIoCompletionCallback(

DWORD dwErrorCode,

DWORD dwNumberOfBytesTransfererred,

DWORD dwFlags,

POVERLAPPED pOverLapped);

这里多了一个OVERLAPPED结构传递给函数,这种结构通常被传递给ReadFile和WriteFile之类的函数,系统在内部始终保持对这个带有待处理IO请求的重叠结构进行跟踪。当请求完成时,系统将该结构的地址放入完成端口,从而使它能够被传递给你的OverLapped

CompletionRoutine函数,应该使用上下文信息放入OVERLAPPED结构的结尾处的传统方法。

如果关闭设备,会导致它的所有待处理的IO请求立即完成,并产生一个错误代码。要做好准备面对这种情况,如果想关闭设备后确保没有运行任何回调函数,可以使用引用计数的特性。

目前没有特别的标志传递给BindIoCompletionCallback的dwFlags参数,只能传递0,或者是WT_EXECUTEINTOTHREAD。如果一个IO请求经完成,它将被排队放入一个非IO组件线程。在OverLappedCompletionRoutine函数中,可以发出一个异步IO请求,只是要小心如果发出IO请求的线程终止运行,IO请求也会被撤销。

非IO组件中的线程是根据工作量来创建或撤销的。如果工作量非常小,该组件中的线程将会终止运行,IO请求却是出于未处理的状态的。如果BindIoCompletionCallback支持WT_EXECUTEINTOTHREAD标志,那么在完成端口上等待的线程将会醒来,并将结果移植到一个IO组件中。由于在IO请求出于未处理的状态下,这些线程不会停止运行,所以不用担心它们被撤销。

虽然EXECUTEINTOTHREAD标志的作用不错,但是很容易模仿相似的特性。在OverLappedCompletionRoutine中,只需要调用QueueUserWorkItem,传递EXECUTEINTOTHREAD标志和想要的任何数据(至少是重叠结构)。

这就是线程池可以为你执行的全部功能。

原文地址:https://www.cnblogs.com/leoTsou/p/12431545.html

时间: 2024-08-27 02:14:17

回炉重造之重读Windows核心编程-011-线程池和其他异步方式的相关文章

回炉重造之重读Windows核心编程-006-线程

线程也是有两部分组成的: 线程的内核对象,操作系统用来管理线程和统计线程信息的地方. 线程堆栈,用于维护现场在执行代码的时候用到的所有函数参数和局部变量. 进程是线程的容器,如果进程中有一个以上的线程,这些线程将共享进程的地址空间,操作空间中的数据,执行相同的代码,对相同的数据操作,甚至内核对象句柄(因为它是依托进程而不是线程存在的). 所以进程使用的系统资源比线程多的多,线程只需要一个内核对象和一个堆栈.既然线程比进程需要的开销少,因此始终都应该设法用增加线程来解决编程问题.当然这也不是一成不

回炉重造之重读Windows核心编程-003-内核对象

内核对象是个比较难理解的概念,问题的根源就在于即使是<核心编程>书中也没有说清楚它的定义,只是不停地举例和描述它的性质,还有如何使用. 盲人摸象,难见全貌.只能尽可能列举它的性质,注意使用了. 引用计数(书中的说法是使用计数)就是内核对象的一个很关键的性质.由于内核对象的拥有者是内核而不是进程,所以只能由内核来做撤销内核对象的操作.而通常一个内核对象不一定只被一个进程使用的,创建或者撤销内核对象,就要看引用计数了.引用计数在内核对象被创建的时候被置为1,被进程访问一次引用计数就递增1.当引用计

回炉重造之重读Windows核心编程-001-错误处理

Windows处理错误靠的是API的返回值,类型不止一种种: VOID,函数不可能失败,Windows API的返回值很少是这个情况. BOOL,如果函数失败,则返回值是0,否则返回是非零值.不要测试返回值是否为TRUE! HANDLE,如果函数失败,则返回值通常是NULL,否则返回一个HANDLE用于操作对象.有的函数是返回INVALID_HANDLE_VALUE的,它被定义为-1,以函数在文档中的说明为标准! PVOID,如果函数失败,则返回NULL,否则返回内存块的地址. LONG/DWO

回炉重造之重读Windows核心编程-007-线程的调度、优先级与亲缘性

Windows被设计成一个抢占式的操作系统,用某种算法来确定哪些线程应该在何时被调度和运行多长时间.每隔20ms左右,Windows就要查看当前所有线程的内核对象,找到可以被调度的一个,将它加载到CPU寄存器中.这个操作成为上下文切换.Windows实际上保存了一个记录,说明每个线程获得了多少次运行的机会.使用MicrosoftSpy++这个工具可以了解这个情况. 一个线程随时可以停止运行,一个线程可以进行调度.可以对线程进行一定程度的控制,但是不能太多.不能保证一个线程做任何事. 7.1暂停和

回炉重造之重读Windows核心编程-014-虚拟内存

14.1 系统信息 GetSystemInfo函数用户检索与主机相关的值,只需要传递SYSTEM_INFO结构体的地址即可. typedef struct _SYSTEM_INFO{ union{ DWORD dwOemId; struct { WORD wProcessorArchiteture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplica

【windows核心编程】线程局部存储TLS

线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来使用动态TLS, 这些函数实际上最为DLL所使用. 系统中的每个进程都有一组 正在使用标志(in-use flag), 每个标志可被设置为FREE 或者 INUSE, 表示该TLS元素是否正在使用. 微软平台保证至少有TLS_MINUMUM_AVALIABLE个标志位可供使用, TLS_MINUMU

【转】《windows核心编程》读书笔记

这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLastError可能用于描述成功的原因(CreatEvent)

【windows核心编程】DLL相关(3)

DLL重定向 因为DLL的搜索路径有先后次序,假设有这样的场景:App1.exe使用MyDll1.0.dll, App2.exe使用MyDll2.0.dll, MyDll1.0 和 MyDll2.0是同一个DLL的两个版本,1.0为旧版本,2.0为新版本. 而如果MyDll2.0.dll的存放路径的优先次序比较靠前时,那么App1.exe就会去加载MyDll2.0.dll,这就可能引发 DLL地狱问题,因此DLL重定向可解决这个问题. 加载程序总是先检查应用程序目录,我们所要做的就是如下: ①在

windows核心编程 DLL技术 【转】

注:本文章转载于网络,源地址为:http://blog.csdn.net/ithzhang/article/details/7051558 本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入,此种方式被称为隐式链接. 第二种方式是在程序运行时,通过调用API显式的载入所需要的DLL,并显式的链接所想要链接的符号.换句话说,程序在运行时,其中的一个线程