十一、Windows线程池
Windows提供了一个(与I/O完成端口相配套的)线程池机制来简化线程的创建、销毁以及日常管理。这个通用的线程池可能并不适用于所有的情况,但很多情况下能够满足需要并节省大量的开发时间。
这些新的线程池函数允许我们做以下这些事情:
1、以异步的方式来调用一个函数
2、每隔一段时间调用一个函数
3、当内核对象触发的时候调用一个函数
4、当异步I/O请求完成的时候调用一个函数
当一个进程初始化的时候,它并没有任何与线程池有关的开销。一旦调用了新的线程池函数,系统会为进程创建相应的内核资源,其中的一些资源在进程终止之前一直存在。
1、以异步方式调用函数
为了让线程池中的一个线程执行调用函数,需要向线程池提交一个请求,为此只需要调用下面的函数:
BOOL TrySubmitThreadpoolCallback(
__in PTP_SIMPLE_CALLBACK pfns,
__inout_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
该函数(通过PostQueuedCompletionStatus)将一个工作项(work item)添加到线程池的队列中。系统会自动为我们的进程创建一个默认的线程池,并让线程池中的一个线程来调用回调函数。
每一次调用TrySubmitThreadPoolCallback时,系统会在内部以我们的名义分配一个工作项。如果打算提交大量的工作项,那么出于性能和内存使用的考虑,创建工作项一次,然后多次提交会更好,可以用下面的函数创建一个工作项:
PTP_WORK CreateThreadpoolWork(
__in PTP_WORK_CALLBACK pfnwk,
__inout_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
当我们想要向线程池提交一个请求的时候,可以调用SubmitThreadpoolWork函数。
如果我们有另一个线程,该线程想要取消已经提交的工作项,或者该线程由于要等待工作项处理完毕而需要将自己挂起,那么可以调用以下的函数:
VOID WaitForThreadpoolWorkCallbacks(
__inout PTP_WORK pwk,
__in BOOL fCancelPendingCallbacks
);
不再需要一个工作项的时候,应该调用CloseThreadPoolWork。
2、每隔一段时间调用一个函数
首先定义这个回调函数,然后调用下面的函数通知线程池在何时调用我们的函数:
PTP_TIMER CreateThreadpoolTimer(
__in PTP_TIMER_CALLBACK pfnti,
__inout_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
想要向线程池注册计时器的时候,调用SetThreadpoolTimer函数,它也可以用来修改已有了的计时器。
可以通过调用IsThreadpoolTimerSet来查看某个计时器是否已经被设置。
相应的,WatiForThreadpoolTimerCallbacks来让线程等待一个计时器完成,还可以调用CloseThreadpoolTimer来释放计时器的内存。
3、在内核对象触发时调用一个函数
如果想到注册一个工作项,让它在一个内核对象被触发的时候执行,那么,首先编写这个调用函数,然后调用CreateThreadpoolWait来创建一个线程池等待对象,创建完成后,调用函数SetThreadpoolWait来将一个内核对象绑定到这个线程池。
线程池在内部会让一个线程调用WaitForMultipleObjects函数,传入通过SetThreadpoolWait函数注册的一组句柄,并传FALSE给bWaitAll参数。这样任何一个句柄被触发,线程池就会被唤醒。
最后,可以通过调用WaitForThreadpoolWaitCallbacks函数来等待一个等待项完成,还可以通过调用CloseThreadpoolWait函数来释放一个等待项的内存。
4、在异步I/O请求完成时调用一个函数
首先,必须编写一个调用函数,当I/O操作完成的时候,这个函数会被调用并得到一个指向OVERLAPPED结构的指针,这个指针是我们在调用ReadFile或WriteFile时传入的。
然后,我们调用CreateThreadpoolIo来创建一个线程池I/O对象,并将我们想要与线程池内部的I/O完成端口相关联的文件/设备句柄当第一个参数传入,创建完毕之后,调用函数StartThreadpoolIo将嵌入在I/O项中的文件/设备与线程池内部的I/O完成端口关联起来。
在每次调用ReadFile和WriteFile之前,都必须调用StartThreadpoolIo,否则回调函数不会被调用。
如果想在发生I/O请求之后让线程池停止调用我们的回调函数,可以调用函数CancelThreadpoolIo。
当对文件/设备的使用完成后,应该调用CloseThreadpoolIo解除它与线程池的关系。
函数WaitForThreadpoolIoCallbacks用来让另一个线程等待一个等处理的I/O请求完成。
十二、纤程
应该尽量避免使用纤程,尽量对应用程序进行合理的设计,使之能够使用Windows提供的线程。纤程是为了帮助将UNIX代码移植到WINDOWS中增加的,因为UNIX服务器应用程序是单线程的,但创建了自己的线程框架函数库,可以用来模拟纯粹的线程。这个线程包能够创建多个栈,保存某些CPU寄存器,并能够在它们之间进行切换来对客户请求进行服务。
纤程是在用户模式下实现的,内核对纤程一无所知,内核会根据我们自定义的算法来对纤程进行调试。由于纤程调用算法是我们定义的,因此就内核而言,它对纤程的调度不是抢占式的、
一个线程可以包含一个或多个纤程。线程一次只能执行一个纤程的代码。
使用纤程的第一个步骤是将一个已有的线程转换为一个纤程。可以通过函数ConvertThreadToFiber来达到目的,这个函数会为纤程的执行上下文分配内存(约200字节),它返回的实际上是纤程的执行上下文的内存地址。
除非我们打算创建更多的纤程,并让它们在同一个线程中运行,否则没有理由将一个线程转换为纤程。为了创建另一个纤程,线程(即当前正在运行的那个纤程)应该调用CreateFiber,它返回的是纤程执行上下文的内存地址。但与ConvertThreadToFiber不同的是,这个新的纤程不会执行,为了让新的纤程执行,应该调用SwitchToFiber,它是让纤程得到CPU时间的唯一方法。
调用DeleteFiber来销毁纤程,用GetCurrentFiber得到当前运行纤程的执行上下文的地址。GetFiberData将创建或转换纤程时传入的参数值返回给纤程回调函数。