知乎:https://www.zhihu.com/people/monkey.d.luffy Android高级开发交流群2: 752871516
全部博文(315)
分类: WINDOWS
2013-10-06 21:43:07
多线程程序设计是Windows程序设计的难点之一。为帮助进行多线程程序设计,Windows提供了线程池机制。《Windows核心编程》的第十一章详细介绍了线程池的使用方法。最近再次阅读这一章,比先前囫囵吞枣的阅读理解深多了,正是所谓的“读书三遍,其义自现”。“纸上得来终觉浅,绝知此事要躬行”,为此,写了个简单的文件复制程序,实验各种线程池机制的使用方法。程序使用了《Windows核心编程》第十一章介绍的线程池提供的四种回调机制中的三种以及异步过程调用(APC)机制和待命等待,虽然用得不太自然,但作为入门的示例,应该是不错的。
0 概述
引用MSDN 2005对线程池进行概括介绍的一篇文档(ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.WIN32COM.v10.en/dllproc/base/thread_pooling.htm)作为开篇理论基础:
很多程序创建在睡眠状态消耗大量时间等待某事件发生的线程,还有一些线程可能会进入睡眠状态,只是不时地被唤醒来轮询状态信息的改变或者更新状态信息。线程池为程序提供由系统管理的还有工作线程的池,让程序可以更有效地使用线程。至少有一个线程监视排队到线程池的所有等待操作的状态。等待操作完成时,线程池中的某个工作线程会执行相应的回调函数。
也可以把与等待操作无关的工作项目排队到线程池。调用QueueUserWorkItem函数就可以要求线程池中的线程处理某工作项目。这个函数要求一个将被线程池中选中线程调用的函数(指针)作为参数。无法取消已经排队的工作项目。
定时器队列定时器(Timer-queue
timers)和注册的等待操作(registered wait operations)也使用线程池,因为它们的回调函数是被排队到线程池的。也可以使用BindIoCompletionCallback函数发送异步I/O操作到线程池中,I/O操作完成后,线程池中某线程会执行(异步I/O完成)回调函数。
线程池在首次调用QueueUserWorkItem或者BindIoCompletionCallback时,或者某定时器队列定时器、注册的等待操作对回调函数排队时被创建。缺省情况下,线程池中可创建的线程数目大约是500。每个线程使用默认的栈尺寸,在默认优先级运行。
线程池中有两种类型的工作线程:I/O线程和非I/O线程。I/O工作线程在待命等待状态进行等待。排队到I/O工作线程的工作项目作为异步过程调用执行。如果工作项目应该在以待命等待状态等待的线程中执行,就应该排队到I/O工作线程中。
非I/O工作线程在I/O完成端口上等待。使用非I/O工作线程比使用I/O工作线程更高效。因此,应该尽可能地使用非I/O工作线程。如果有未决的异步I/O请求存在,I/O和非I/O工作线程都不会退出。两种类型的线程都可以用于需要发起异步I/O完成请求的工作项目。然而,应该避免在非I/O工作线程中发送需要很长时间才能完成的异步I/O完成请求。
要使用线程池,工作项目以及它们调用的函数都应该是线程池安全的。线程池安全函数不假定执行它的线程是专用或者永久的。一般来说,应该避免(在工作项目中)使用线程局部存储或者对要求永久线程的异步调用,如RegNotifyChangeKeyValue,进行排队。然而,通过使用QueueUserWorkItem和WT_EXECUTEINPERSISTENTTHREAD选项,可以把这些函数排队到永久工作线程中。
注意线程池与单线程套间模型(single-threaded apartment model)不兼容。
这段文字简要介绍了四种线程池回调机制和两种工作线程。小结如下:
四种线程池回调机制是:
两种工作线程为:
ReadFileEx和WriteFileEx是通过异步过程调用来进行I/O操作完成通知的,所以不能在排队到非I/O工作线程的工作项目回调函数中调用ReadFileEx或者WriteFileEx来进行I/O操作。QueueUserWorkItem默认(第三个参数为0时)将工作项目排队到非I/O工作线程中,因为它的效率更高。而I/O工作线程效率较低,只应该在回调函数产生对当前线程的异步过程调用请求(比如说,调用ReadFileEx或者WriteFileEx时)时使用,并且应该在线程返回到线程池后再执行异步过程调用请求。
1 总体设计
目标是设计一个简单的使用线程池进行多线程文件复制的程序,以练习线程池的使用。最终设计的文件复制接口如下:
struct
FileCopy
HANDLE CreateFileCopyTask(const
FileCopy&); |
使用方法为:填充FileCopy结构体,指定要复制的源文件和复制到的目录,调用CreateFileCopyTask()创建文件复制句柄,调用StartFileCopy()开始文件复制;如果要取消复制,可调用StopFileCopy();复制完成后调用DestroyFileCopy()销毁文件复制句柄。
文件复制的过程为:
通常的多线程程序设计中,这需要创建并且管理多个工作线程,是比较麻烦的。而如果使用线程池中的工作线程,则程序员可以只关注具体的业务操作(文件复制),而将线程创建和管理任务交给操作系统完成。在实现用线程池中的工作线程进行文件复制时,采用了下列定义:
enum TotalFileState
enum ChunkCopyState
// 文件复制状态
HANDLE
hAllDoneEvent; //
表示复制完成的事件
DWORD
dwFileSize; //
要复制的文件尺寸 struct FileCopyContext;
// 文件数据块复制任务
// 文件复制上下文 |
上文提到的文件复制句柄,实际上是FileCopyContext结构体指针。结构体的最后一个字段是只有一个元素的FileCopyTask结构体数组,然而在分配FileCopyContext结构体的时候,会根据所需要的工作线程数目,在结构体后面再多分配一些内存,用于保存分配给其他工作线程的数据块复制任务(FileCopyTask)结构体。这些结构体用task_array数组下标大于0的元素表示。这是一种常用的处理可变尺寸结构体的方法,从Windows
API函数设计学习而来。
http://blog.sina.com.cn/s/blog_56dee71a0100h6au.html