多线程程序设计是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)不兼容。
这段文字简要介绍了四种线程池回调机制和两种工作线程。小结如下:
四种线程池回调机制是:
异步调用函数:通过QueueUserWorkItem要求异步调用某函数。
定时器队列定时器:每隔一定时间,线程池中某工作线程就会调用指定的回调函数,相关函数为CreateTimerQueue,CreateTimerQueueTimer等。
注册的等待对象:某内核对象(信号量、事件、进程、控制台输入等)授信时调用指定的回调函数,相关函数为RegisterWaitForSingleObject,UnregisterWaitEx等。
异步I/O操作完成回调函数:某异步I/O操作完成时调用指定的回调函数,相关函数为BindIoCompletionCallback。
两种工作线程为:
I/O工作线程:线程在待命等待态进行等待,所以可以处理(对本线程的)异步过程调用请求,即可以在回调函数中发起对当前线程的异步过程调用请求。
非I/O工作线程:线程在完成端口上进行等待,不能处理异步过程调用请求,不能在回调函数中发起对当前线程的异步过程调用请求。
ReadFileEx和WriteFileEx是通过异步过程调用来进行I/O操作完成通知的,所以不能在排队到非I/O工作线程的工作项目回调函数中调用ReadFileEx或者WriteFileEx来进行I/O操作。QueueUserWorkItem默认(第三个参数为0时)将工作项目排队到非I/O工作线程中,因为它的效率更高。而I/O工作线程效率较低,只应该在回调函数产生对当前线程的异步过程调用请求(比如说,调用ReadFileEx或者WriteFileEx时)时使用,并且应该在线程返回到线程池后再执行异步过程调用请求。
1
总体设计
目标是设计一个简单的使用线程池进行多线程文件复制的程序,以练习线程池的使用。最终设计的文件复制接口如下:
struct
FileCopy
{
TCHAR
szSrcFile[MAX_PATH]; // 源文件
TCHAR
szDstDir[MAX_PATH]; // 目标目录
UINT
nBufSize;
// 每个线程缓冲区大小(每次复制的文件数据大小)
UINT
nThreadCnt; //
线程数(>0)
UINT
nTimeOut;
// 超时值,单位为ms,0表示不超时.
};
HANDLE CreateFileCopyTask(const
FileCopy&);
int DestroyFileCopy(HANDLE hFileCopy);
BOOL
StartFileCopy(HANDLE);
int
StopFileCopy(HANDLE);
使用方法为:填充FileCopy结构体,指定要复制的源文件和复制到的目录,调用CreateFileCopyTask()创建文件复制句柄,调用StartFileCopy()开始文件复制;如果要取消复制,可调用StopFileCopy();复制完成后调用DestroyFileCopy()销毁文件复制句柄。
文件复制的过程为:
启动文件复制时,为每个线程分配一个文件数据块复制任务;
一旦线程完成了当前分配给它的任务,就更新文件复制进度,如果还有待复制的文件数据块,则再为线程分配一个数据块复制任务;
重复上一步,直至整个文件的所有数据块复制任务分配完成,而且所有线程完成了分配给它的任务,则整个文件复制完成。
通常的多线程程序设计中,这需要创建并且管理多个工作线程,是比较麻烦的。而如果使用线程池中的工作线程,则程序员可以只关注具体的业务操作(文件复制),而将线程创建和管理任务交给操作系统完成。在实现用线程池中的工作线程进行文件复制时,采用了下列定义:
enum TotalFileState
{
FILE_COPY_STOPPED,
FILE_COPY_RUNNING,
FILE_COPY_PAUSED,
FILE_COPY_DONE,
FILE_COPY_CANCELED,
FILE_COPY_TIMEOUT,
};
enum ChunkCopyState
{
COPY_STATE_IDLE,
COPY_STATE_READ_SRC,
COPY_STATE_WRITE_DST,
COPY_STATE_DONE,
COPY_STATE_ERROR,
COPY_STATE_CANCELED,
};
// 文件复制状态
struct
FileCopyState
{
CRITICAL_SECTION
cs; //
控制对状态信息的互斥访问
HANDLE
hCallerThread; //
调用者线程
TotalFileState
dwState; //
整个文件复制状态
BOOL
bAutoDelete;
// 在操作进行期间请求销毁句柄,则设置自动删除标志.
HANDLE
hAllDoneEvent; //
表示复制完成的事件
HANDLE
hWaitForAllDone; //
等待复制完成事件
DWORD
nPendingIOCnt; //
未决I/O操作数
DWORD
dwIOErrorCnt;
// I/O操作错误计数
DWORD
dwFileSize; //
要复制的文件尺寸
DWORD
dwNextIdlePos; //
下一次要复制的数据块起始位置
DWORD
dwDoneSize; //
已经复制完的数据量
DWORD
dwChunkSize;
// 每次复制的数据量大小
};
struct FileCopyContext;
// 文件数据块复制任务
struct
FileCopyTask
{
OVERLAPPED
osStruct; //
用于重叠I/O操作的结构体
FileCopyContext*
pCtx; // 文件复制上下文
ChunkCopyState dwState; //
当前数据块复制状态
HANDLE
hSrcFile; //
源文件
HANDLE
hDstFile; //
目标文件
DWORD
dwStartPos; //
当前数据块起始地址(相对于源/目标文件)
DWORD
dwDataSize; //
当前数据块大小
DWORD
dwDoneSize; //
已经完成读/写操作的数据量
DWORD
dwIOError; //
I/O错误代码
BYTE*
pBuf;
// I/O操作缓冲区
};
// 文件复制上下文
struct
FileCopyContext
{
FileCopy
file_copy; //
用户请求
FileCopyState
copy_state; //
操作状态
FileCopyTask
task_array[1]; //
文件数据块复制任务数组
};
上文提到的文件复制句柄,实际上是FileCopyContext结构体指针。结构体的最后一个字段是只有一个元素的FileCopyTask结构体数组,然而在分配FileCopyContext结构体的时候,会根据所需要的工作线程数目,在结构体后面再多分配一些内存,用于保存分配给其他工作线程的数据块复制任务(FileCopyTask)结构体。这些结构体用task_array数组下标大于0的元素表示。这是一种常用的处理可变尺寸结构体的方法,从Windows
API函数设计学习而来。
http://blog.sina.com.cn/s/blog_56dee71a0100h6au.html