在编程世界中,文件操作是基础且常见的任务之一。对于初学者来说,通常从创建、读取或写入文件的示例开始。在Windows系统中,所有与文件相关的调用最终都会用到CreateFile、ReadFile或WriteFile等API。这些同步API在默认情况下(未指定OVERLAPPED参数)只有在请求的数据被读取或写入后才返回。然而,本文的主题是讨论异步版本的API:ReadFileEx和WriteFileEx。实际上,ReadFile和WriteFile也可以表现为异步操作,但本文将专注于明确的异步API ReadFileEx和WriteFileEx。
通常情况下,当需要异步行为或并行处理时,会创建线程。异步文件I/O API允许在不引入线程的情况下利用异步行为。可以发出ReadFileEx或WriteFileEx调用,然后执行其他操作。最后,当应用程序需要I/O操作的结果时,它可以通过一些API检查异步操作的状态,将在下面讨论这些API。为了使用异步API,文件应该用FILE_FLAG_OVERLAPPED标志打开。
下面是ReadFileEx的原型,WriteFileEx与之相同。对最后两个参数lpOverlapped和lpCompletionRoutine感兴趣。需要指定一个OVERLAPPED结构,其中包含从哪里开始读取或写入数据的偏移量。这个结构中有一个hEvent成员,MSDN文档表示可以自由使用它。但是,当ReadFile和WriteFile的最后一个参数OVERLAPPED指针被提供时,会表现出异步行为。在这种情况下,一些MSDN文档表示需要为每个OVERLAPPED结构实例提供单独的事件句柄。如果查看FileIOCompletionRoutine的文档,在备注中,它要求即使dwErrorCode为零也要调用GetOverlappedResult。但是,如果查看GetOverlappedResult的文档,它提到了为每个传递的OVERLAPPED指针指定一个手动重置事件。GetOverlappedResult等待事件被触发。但是,它只适用于异步版本的ReadFile和WriteFile是有意义的。在由ReadFileEx或WriteFileEx启动的重叠操作的情况下,系统可能有一个不同的机制来执行GetOverlappedResult,而不是事件。在ReadFileEx和WriteFileEx的情况下,lpCompletionRoutine是系统在指定的异步操作完成时调用的回调函数。它将为每个ReadFileEx/WriteFileEx调用被调用。这里有一个旁注。这个回调例程只有在线程进入“可提醒”等待状态时才会被调用。这个状态可以通过SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx等设置。一个线程可以发出异步I/O操作并自由地做其他工作。然后,一旦它完成了所有其他任务,它可以进入可提醒等待状态。然后,回调例程在这些API的上下文中被调用。可以使用GetOverlappedResult API检查重叠操作的状态。
将所有这些知识封装到一个名为AsyncFile的小类中。函数都有良好的注释,所以很容易理解每个函数的目的。
class IAsyncOperationCompletionNotifier {
public:
virtual void OnAsyncOperationComplete(BOOL bRead, DWORD dwErrorCode) = 0;
};
class AsyncFile {
public:
AsyncFile(LPCTSTR lpctszFileName, BOOL bCreate, DWORD dwDesiredAccess,
DWORD dwShareMode, IAsyncOperationCompletionNotifier* pINotifier,
BOOL bSequentialMode = FALSE,
__int64 nStartOffset = 0,
BOOL bInUIThread = FALSE);
BOOL IsOpen();
BOOL Write(LPVOID pvBuffer, DWORD dwBufLen, DWORD dwOffsetLow = 0,
DWORD dwOffsetHigh = 0);
BOOL Read(LPVOID pvBuffer, DWORD dwBufLen, DWORD dwOffsetLow = 0,
DWORD dwOffsetHigh = 0);
BOOL IsAsyncIOComplete(BOOL bFlushBuffers = TRUE);
BOOL AbortIO();
DWORD GetFileLength(DWORD* pdwOffsetHigh);
__int64 GetLargeFileLength();
void Reset(BOOL bSequentialMode = FALSE,
__int64 nStartOffset = 0);
operator HANDLE() {
return m_hAsyncFile;
}
BOOL SeekFile(__int64 nBytesToSeek, __int64& nNewOffset, DWORD dwSeekOption);
~AsyncFile(void);
private:
// ...
};
类对象可以在指定文件名、是否需要创建新文件、访问模式(读/写)、共享模式(与CreateFile相同)、注册I/O完成通知的回调接口、是否为顺序模式、起始偏移量,以及线程是否托管UI的情况下创建。对于UI线程,使用MsgWaitForMultipleObjectEx API进行I/O完成等待(设置可提醒等待状态)。对于非UI线程,使用WaitForSingleObjectEx。使用事件m_hIOCompleteEvent来同步等待。它是一个手动重置事件,应该按照MSDN文档。这是因为,如果事件是自动重置的,等待函数可以改变其状态。这可能会导致意外的死锁,如果调用GetOverlappedResult并将其等待完成标志设置为TRUE。m_hIOCompleteEvent在每个异步请求的完成例程被调用时被设置。实际上,对于每个发出的异步调用都有一个计数器。当完成例程被调用时,这个计数器会递减。当它达到零时,事件被触发。
Read/Write函数执行重叠I/O操作。如果顺序模式标志为TRUE,则偏移量将通过请求的缓冲区长度递增。在发出Read/Write请求后,可以调用IsAsyncIOComplete函数来触发可提醒等待状态,并获取排队的I/O完成例程被执行。这个函数通过调用GetOverlappedResult来检查每个请求的异步操作的状态。此外,如果bFlushBuffers标志为TRUE,它将使用FlushFileBuffers API将文件缓冲区刷新到磁盘。这可能是必要的,因为系统将数据写入中间缓存以提高性能。这些缓存的内容稍后会被刷新到磁盘。这种机制提高了应用程序的性能,但存在数据丢失的风险(如果由于电源故障或其他原因发生意外关闭)。
可以通过发出CancelIo API来取消异步I/O操作。这只会在调用API的线程执行异步I/O时成功。AbortIO调用可以用来取消IO。
还准备了一个演示应用程序(提供了VS2010解决方案和VC6.0 dsp)。这个应用程序接受一个源文件并将内容复制到目标文件。它使用AsyncFile的一个实例来读取源文件,另一个实例来写入目标文件。复制的进度在UI中可见。此外,用户可以在中间取消操作。在Win7上测试了这个应用程序,并在XP上进行了一些测试。调试跟踪可以在DebugView中看到。就这些了!谢谢!