多线程程序设计是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函数设计学习而来。
2
详细设计 2.1
数据块复制
工作线程的工作是完成分配给它的文件数据块复制任务,这涉及到以下几个函数:
2.1.1
分配文件数据块复制任务函数
// 分配下一个文件数据块复制任务 static int AssignNextChunkCopyTask(FileCopyContext*
pCtx,FileCopyTask* pTask) { assert(pCtx != NULL); assert(pTask
!= NULL); EnterCriticalSection(&(pCtx->copy_state.cs)); // 复制任务已经完成,或者任务超时,或者已经请求取消/暂停 if
((FILE_COPY_RUNNING !=
pCtx->copy_state.dwState) || (pCtx->copy_state.dwDoneSize ==
pCtx->copy_state.dwFileSize)) { if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { pCtx->copy_state.dwState = FILE_COPY_DONE; } if (0 == pCtx->copy_state.nPendingIOCnt) { SetEvent(pCtx->copy_state.hAllDoneEvent); } LeaveCriticalSection(&(pCtx->copy_state.cs)); return -2; } // 分配下一个文件数据块复制任务 DWORD dwSize; if (pCtx->copy_state.dwNextIdlePos +
pCtx->copy_state.dwChunkSize <=
pCtx->copy_state.dwFileSize) { dwSize = pCtx->copy_state.dwChunkSize; } else if
(pCtx->copy_state.dwNextIdlePos <
pCtx->copy_state.dwFileSize) { dwSize = pCtx->copy_state.dwFileSize -
pCtx->copy_state.dwNextIdlePos; } else { LeaveCriticalSection(&(pCtx->copy_state.cs)); return 0; } pTask->pCtx = pCtx; pTask->dwState = COPY_STATE_READ_SRC; pTask->dwIOError = 0; pTask->dwStartPos =
pCtx->copy_state.dwNextIdlePos; pTask->dwDataSize = dwSize; pTask->dwDoneSize = 0; pCtx->copy_state.dwNextIdlePos += dwSize; pCtx->copy_state.nPendingIOCnt++; QueueUserWorkItem(OnChunkCopyProgress,pTask,0); LeaveCriticalSection(&(pCtx->copy_state.cs)); return 0; }
|
函数首先检查整个复制任务状态,如果任务已经被取消、暂停(还未实现)或者已经完成,则不进行下面的分配数据块复制任务操作。整个文件复制任务的状态由
FileCopyContext结构体的copy_state字段的dwState成员表示。如果请求取消复制任务,会设置此成员值为
FILE_COPY_CANCELED;文件复制支持超时操作,如果超过指定时间还没有完成整个文件复制,则认为操作超时,会设置dwState成员值为
FILE_COPY_TIMEOUT。注意:执行SetEvent(pCtx->copy_state.hAllDoneEvent);
表示整个文件复制完成(正确地完成、被用户取消、因为超时被取消等),执行这个语句的条件是0 ==
pCtx->copy_state.nPendingIOCnt,即没有未决的I/O操作,所有工作线程的任务已经完成(正确地完成或者被取消)并
且退出。下面在讨论退出处理时还会详细介绍这段代码。
如
果没有退出函数处理,则接下来为工作线程分配任务,即设置FileCopyContext结构体的task_array成员的某个元素的各个字段值。任务
分配后调用QueueUserWorkItem(OnChunkCopyProgress,pTask,0)将
其排队到线程池中。前两个参数的意义是,要求线程池中的某个工作线程以参数pTask调用工作项目回调函数OnChunkCopyProgress;第三
个参数为0,表示使用默认选项,即在非I/O工作线程中调用工作项目回调函数。
2.1.2
文件数据块复制工作项目回调函数
// 文件复制进度通知回调函数 static DWORD CALLBACK OnChunkCopyProgress(LPVOID param) { FileCopyTask* pTask = (FileCopyTask*)param; FileCopyContext* pCtx = pTask->pCtx; LPOVERLAPPED
pStruct = &(pTask->osStruct); DWORD
dwIOError = 0; BOOL bTaskDone = FALSE; EnterCriticalSection(&(pCtx->copy_state.cs)); // 发生I/O错误 if (0 !=
pTask->dwIOError) { dwIOError = pTask->dwIOError; } else if
(COPY_STATE_READ_SRC == pTask->dwState) { // 读取数据完成,向目标文件写入数据 if (pTask->dwDataSize ==
pTask->dwDoneSize) { pTask->dwState = COPY_STATE_WRITE_DST; pTask->dwDoneSize = 0; pStruct->Offset = pTask->dwStartPos; if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { QueueUserWorkItem(OnChunkCopyProgress,pTask,0); } } // 读取部分数据完成,继续读取剩余部分 else if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { pStruct->Offset = pTask->dwStartPos +
pTask->dwDoneSize; if (!ReadFile(pTask->hSrcFile, pTask->pBuf +
pTask->dwDoneSize, pTask->dwDataSize -
pTask->dwDoneSize, NULL,pStruct)) { dwIOError = GetLastError(); } } } else if
(COPY_STATE_WRITE_DST == pTask->dwState) { // 写入部分数据完成,继续写入剩余部分 if (pTask->dwDoneSize <
pTask->dwDataSize) { pStruct->Offset = pTask->dwStartPos +
pTask->dwDoneSize; if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { if (!WriteFile(pTask->hDstFile, pTask->pBuf +
pTask->dwDoneSize, pTask->dwDataSize -
pTask->dwDoneSize, NULL,pStruct)) { dwIOError = GetLastError(); } } } // 复制数据块完成 else { bTaskDone = TRUE; } } // 已经请求取消/暂停,或者复制操作超时 if
(FILE_COPY_RUNNING !=
pCtx->copy_state.dwState) { dwIOError = ERROR_OPERATION_ABORTED; } // 文件块复制已经完成,或者发生I/O错误,决定下一步的动作 if
(bTaskDone || ((0 != dwIOError) &&
(ERROR_IO_PENDING != dwIOError))) { pTask->dwIOError = dwIOError; pTask->dwState = COPY_STATE_DONE; if (0 == OnChunkCopyDone(pCtx,pTask)) { AssignNextChunkCopyTask(pCtx,pTask); } } LeaveCriticalSection(&(pCtx->copy_state.cs)); return 0; } |
这个函数的任务是进行从源文件读取数据和向目标文件写入数据的操作。因为源文件和目标文件都是以重叠方式(调用CreateFile时带FILE_FLAG_OVERLAPPED标
志)打开的,所以函数中对ReadFile和WriteFile的调用都是以异步方式进行操作的。这时,如果I/O操作不能立即完成,通常会以错误码ERROR_IO_PENDING返
回,表示操作已经成功发起,但需要等待外设完成操作。外设I/O操作完成时,系统(驱动程序)会以某种方式通知应用程序。由于在创建文件复制任务(调用
CreateFileCopyTask)时已经通过BindIoCompletionCallback将源文件和目标文件句柄绑
定到线程池中的完成端口上,所以这里的I/O操作(读写文件)完成通知方式是向完成端口发送一个I/O完成通知,然后由线程池中的工作线程调用相关回调函
数。CreateFileCopyTask中对BindIoCompletionCallback的调用代码片段如下:
// 绑定到线程池中的完成端口上 if
(!BindIoCompletionCallback(tmp_task.hSrcFile,OnChunkCopyIODone,0) ||
!BindIoCompletionCallback(tmp_task.hDstFile,OnChunkCopyIODone,0)) { CloseHandle(tmp_task.hSrcFile); CloseHandle(tmp_task.hDstFile); return NULL; } |
BindIoCompletionCallback的
第一个参数是文件句柄;第二个参数是I/O完成通知回调函数OnChunkCopyIODone;第三个是保留参数,应该设置为零。《Windows核心
编程》第十一章末尾讨论了这个保留参数将来的可能取值是WT_EXECUTEINIOTHREAD。
2.1.3
I/O完成通知函数
前面提及的I/O完成通知函数OnChunkCopyIODone代码如下:
// 文件I/O完成回调函数 static void CALLBACK OnChunkCopyIODone(DWORD dwError,DWORD
dwBytes,LPOVERLAPPED pStruct) { FileCopyTask* pTask = (FileCopyTask*)pStruct; assert(pTask != NULL); if (0 != dwError) { pTask->dwIOError = dwError; } else { pTask->dwIOError = 0; pTask->dwDoneSize += dwBytes; } OnChunkCopyProgress(pTask); } |
上面的OnChunkCopyProgress()函数通过ReadFile或者WriteFile发起的重叠I/O操作完成后,该回调函数被线程池中某
(在完成端口上等待的)线程调用。
2.1.4
文件数据块复制完成处理函数
// 文件数据块复制完成,确定下一步的动作 static int OnChunkCopyDone(FileCopyContext* pCtx,FileCopyTask*
pTask) { assert(pTask
!= NULL); assert(pCtx != NULL); EnterCriticalSection(&(pCtx->copy_state.cs)); int nRet = 0; pCtx->copy_state.nPendingIOCnt--; assert(pCtx->copy_state.nPendingIOCnt
<=
pCtx->file_copy.nThreadCnt); // 数据块复制完成 if (0 ==
pTask->dwIOError) { pCtx->copy_state.dwDoneSize +=
pTask->dwDataSize; assert(pCtx->copy_state.dwDoneSize <=
pCtx->copy_state.dwFileSize); //PrintDebugString(_T("进度:
%u/%u\n"),pCtx->copy_state.dwDoneSize,pCtx->copy_state.dwFileSize); } //
发生I/O错误,重试. else if
(ERROR_OPERATION_ABORTED != pTask->dwIOError) { pCtx->copy_state.dwIOErrorCnt++; PrintDebugString(_T("!!!!! 任务%p发生I/O错误%u.当前总计发生I/O错误%u次.\n"), pTask,pTask->dwIOError,pCtx->copy_state.dwIOErrorCnt); if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { pTask->dwIOError = 0; pCtx->copy_state.nPendingIOCnt++; QueueUserWorkItem(OnChunkCopyProgress,pTask,0); nRet = -1; } } //
ERROR_OPERATION_ABORTED else { PrintDebugString(_T("***** 任务%p已经取消/暂停/超时\n"),pTask); } LeaveCriticalSection(&(pCtx->copy_state.cs)); return nRet; } |
这
个函数在上文第2小节讨论的函数的末尾被调用,用于在文件数据块复制任务完成,或者发生I/O错误后进行一定的处理:如果数据块复制任务完成,则更新状态
信息(pCtx->copy_state.nPendingIOCnt和pCtx->copy_state.dwDoneSize),并返回
0表示成功,需要分配下一个任务;如果发生I/O错误,则I/O错误计数pCtx->copy_state.dwIOErrorCnt增加,然后清
除错误状态(pTask->dwIOError
=
0)并试图重新进行I/O操作(QueueUserWorkItem(OnChunkCopyProgress,pTask,0))。
2.1.5
关于I/O工作线程和非I/O工作线程的探索
第0节末尾关于两种类型工作线程的论述是根据自己的理解写的,不知道是不是正确。通过简单修改代码进行试验,结果证明那段论述是正确的。试验过程如下:
(1)
修改代码,不使用BindIoCompletionCallback将
源文件和目标文件句柄绑定到异步I/O完成回调函数上,而是使用ReadFileEx和WriteFileEx,在每次读写操作时再指定I/O完成回调函
数。BindIoCompletionCallback是
通过线程池中的完成端口进行异步I/O完成通知的,而ReadFileEx和WriteFileEx是通过对调用者线程发送异步过程调用请求进行异步I
/O完成通知的。具体修改如下:
if (!ReadFileEx(pTask->hSrcFile, pTask->pBuf +
pTask->dwDoneSize, pTask->dwDataSize -
pTask->dwDoneSize, pStruct,OnChunkCopyIODone)) { dwIOError = GetLastError(); } //……………………………… if (!WriteFileEx(pTask->hDstFile, pTask->pBuf
+
pTask->dwDoneSize, pTask->dwDataSize
-
pTask->dwDoneSize, pStruct,OnChunkCopyIODone)) { dwIOError
=
GetLastError(); } |
(2)
重新编译运行程序,发现无法正确进行文件复制了,为何?
与OnChunkCopyProgress()相关的三处QueueUserWorkItem()调用都是
QueueUserWorkItem(OnChunkCopyProgress,pTask,0)
默认选项0表示使用非I/O线程调用回调函数。而回调函数中由ReadFileEx和WriteFileEx发起的异步I/O请求的完成通知是通过向调用
线程(非I/O工作线程)发送异步过程调用请求完成的,但是非I/O工作线程在完成端口上等待,不能处理APC请求,所以在I/O完成后回调函数
OnChunkCopyIODone无法被调用。这样,在完成第一次I/O请求后,后续动作无法进行,所以不能正确进行文件复制了。
(3)
改用I/O工作线程会怎么样?
修改三处与OnChunkCopyProgress()相关的QueueUserWorkItem()调用为
QueueUserWorkItem(OnChunkCopyProgress,pTask,WT_EXECUTEINIOTHREAD);
重新编译运行程序,又可以正确进行文件复制了。选项WT_EXECUTEINIOTHREAD表示要求用I/O工作线程执行工作项目回调函数。I/O工作
线程在待命状态进行等待,可以处理由ReadFileEx和WriteFileEx发起的异步I/O完成时发送的异步过程调用请求,所以文件复制操作可以
正确地进行
2.2 超时/取消/完成处理 程序支持超时处理,即可以指定一个超时值,如果在这个时间内文件复制操作没有完成,则自动取消操作。此外,也支持在复制过程中由用户调用
StopFileCopy()进行取消操作。
2.2.1
超时处理
处理过程为:在启动文件复制时,注册一个等待对象,等待文件复制完成事件;一旦已经经过指定的超时时间,而等待对象没有被取消注册,则相关回调函数会被调
用,完成超时处理。注册等待对象的代码如下:
RegisterWaitForSingleObject( &(pCtx->copy_state.hWaitForAllDone), pCtx->copy_state.hAllDoneEvent, OnFileCopyDone,pCtx, ((0 == pCtx->file_copy.nTimeOut) ? INFINITE :
pCtx->file_copy.nTimeOut), 0); |
第一个参数是句柄指针,用以返回新注册的等待对象;第二个是被等待的内核对象,一旦这个内核对象变为授信状态,则回调函数OnFileCopyDone会
被线程池中某工作线程调用,pCtx会作为参数传给回调函数;第五个参数是等待超时时间,INFINITE表示不使用超时机制;最后一个参数为选项,这里
0表示默认选项,即在非I/O工作线程中调用回调函数。回调函数代码如
下:
// 复制操作已经完成(成功完成/被取消等) static void CALLBACK OnFileCopyDone(PVOID param,BOOLEAN
bTimeout) { FileCopyContext* pCtx = (FileCopyContext*)param; EnterCriticalSection(&(pCtx->copy_state.cs)); if (bTimeout) { if (FILE_COPY_RUNNING ==
pCtx->copy_state.dwState) { PrintDebugString(_T("^^^^^ 超过预定义的时间,文件复制还没有完成,强行取消.\n")); pCtx->copy_state.dwState = FILE_COPY_TIMEOUT; } LeaveCriticalSection(&(pCtx->copy_state.cs)); } else { assert(0 ==
pCtx->copy_state.nPendingIOCnt); switch(pCtx->copy_state.dwState) { case FILE_COPY_DONE: PrintDebugString(_T("^^^^^ 文件复制已经完成.\n")); break; case FILE_COPY_CANCELED: PrintDebugString(_T("^^^^^ 文件复制已经被用户取消.\n")); break; case FILE_COPY_TIMEOUT: PrintDebugString(_T("^^^^^ 文件复制已经因为超时被强行取消.\n")); break; } UnregisterWaitEx(pCtx->copy_state.hWaitForAllDone,NULL); pCtx->copy_state.hWaitForAllDone = NULL;
QueueUserAPC(OnUserNotifyAPCProc,pCtx->copy_state.hCallerThread,ULONG_PTR(pCtx)); LeaveCriticalSection(&(pCtx->copy_state.cs)); } } |
回调函数的第一个参数等于上面调用RegisterWaitForSingleObject()
时的第四个参数;第二个参数表示是否是因为等待超时而调用此回调函数,值TRUE表示因等待超时,被等待内核对象未授信而调用此函数,否则表示被等待内核
对象已经授信。
这个回调函数不仅用于超时处理,还用于取消和完成处理,其中超时处理部分只是简单地设置文件复制状态
为已经超时。这样设置后,2.1.2和2.1.1节介绍的函数会检测到文件复制已经超时,不再继续进行未完成的数据块复制任务,也不再分配新的文件数据块
复制任务。这样,待所有未决的I/O操作都完成后,pCtx->copy_state.nPendingIOCnt值会变为零,最终2.1.1节中
的SetEvent(pCtx->copy_state.hAllDoneEvent)被调用,由RegisterWaitForSingleObject()
注册的等待对象的被等待内核对象变
1