65.9K
CodeProject 正在变化。 阅读更多。
Home

异步文件 I/O (ReadFileEx, WriteFileEx) 的简单封装

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (15投票s)

2011年3月29日

CPOL

6分钟阅读

viewsIcon

93285

downloadIcon

4257

AsyncFile 是一个小型封装类,用于简化异步文件 API 的使用。

引言

本文讨论了一个相对较少被讨论的主题。编程新手大多从创建、读取或写入文件的示例开始。在 Windows 中,所有与文件相关的调用最终都以 CreateFileReadFileWriteFile 结束。我们都熟悉这些 API。从技术上讲,ReadFileWriteFile API 默认是同步 API(当未指定 OVERLAPPED 参数时)。也就是说,这些 API 在请求的数据被读取或写入(默认情况下)之后才会返回。现在,让我们回到文章的主题。这些 API 有纯异步版本:ReadFileExWriteFileEx。实际上,ReadFileWriteFile 也可以表现为异步。但我们将讨论的是显式异步 API ReadFilExWriteFilEx

背景

通常,当我们 D 需要异步行为或并行性时,我们会创建线程。异步文件 I/O API 允许我们在不引入线程的情况下利用异步行为。因此,我们可以发出一个 ReadFileExWriteFileEx 并执行其他操作。然后,当应用程序需要 I/O 结果时,它可以借助稍后将讨论的一些 API 来检查异步操作的状态。为了使用异步 API,文件必须以 FILE_FLAG_OVERLAPPED 标志打开。

下面是 ReadFileEx 的原型。它与 WriteFileEx 相同。

BOOL WINAPI ReadFileEx( 
    __in HANDLE hFile, 
    __out_opt LPVOID lpBuffer, 
    __in DWORD nNumberOfBytesToRead, 
    __inout LPOVERLAPPED lpOverlapped, 
    __in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);

在这里,我们对最后两个参数感兴趣:lpOverlappedlpCompletionRoutine。我们需要指定一个填充了要读取或写入数据的偏移量的 OVERLAPPED 结构。此结构中有一个 hEvent 成员。MSDN 文档说我们可以随意使用它。但是,当 ReadFileWriteFile 的最后一个参数 OVERLAPPED 指针被提供时,会表现出异步行为。在这种情况下,一些 MSDN 文档说我们需要为 OVERLAPPED 结构的每个实例提供单独的事件句柄。如果我们查看 FileIOCompletionRoutine 的文档(http://msdn.microsoft.com/en-us/library/aa364052(v=vs.85).aspx),在备注中,它要求即使 dwErrorCode 为零也要调用 GetOverlappedResult。但是,如果我们查看 GetOverlappedResult 的文档(http://msdn.microsoft.com/en-us/library/ms683209(v=vs.85).aspx),它提到了为每个传递的 OVERLAPPED 指针指定一个手动重置事件。此外,GetOverlappedResult 会等待事件被信号。但是,认为这仅适用于 ReadFileWriteFile 的异步版本是有意义的。对于由 ReadFileExWriteFileEx 启动的重叠操作,系统可能除了事件之外还有其他机制来执行 GetOverlappedResult。对于 ReadFileExWriteFileExlpCompletionRoutine 是系统在指定异步操作完成时要调用的回调函数。它将为每个 ReadFileEx/WriteFileEx 调用调用。这有一个旁路。此回调例程仅在线程进入“可警报”等待状态时调用。可以使用 SleepExWaitForSingleObjectExWaitForMultipleObjectsExMsgWaitForMultipleObjectsEx 等来设置此状态。线程可以发出异步 I/O 操作并自由执行其他工作。然后,一旦它完成了所有其他任务,它就可以进入可警报的等待状态。回调例程随后在这些 API 的上下文中被调用。可以通过 GetOverlappedResult API 来检查重叠操作的状态。

Using the Code

我已经将所有这些知识封装到一个名为 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:
....
};

类对象可以通过指定文件名、是否需要创建新文件、访问模式(READ/WRITE)、共享模式(与 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 调用来取消 I/O。

AsyncFile/Demo_Screenshot2.jpg

此外,我还准备了一个演示应用程序(提供了 VS2010 解决方案和 VC6.0 dsp)。该应用程序接受一个源文件,并将内容复制到目标文件。它使用一个 AsyncFile 实例来读取源文件,并使用另一个实例写入目标文件。复制的进度在 UI 中可见。此外,用户可以随时中止操作。我在 Win7 和 XP 上进行了一定程度的测试。调试跟踪可以在 DebugView 中看到。就这些了!谢谢!

关注点

正如我在开头提到的,异步文件 I/O 不是一个被广泛讨论的主题。因此,AsyncFile 类在对这些 API 进行一些实验后才固定了其设计。我尝试使用一个超过 700 MB 的文件(并且成功了!)。在演示应用程序中,我使用了 AsyncFile 的一个实例进行读取,另一个实例进行写入。我没有尝试使用同一个实例进行读写。期待看到这个类将如何被使用。

更新

  • 修复了指针初始化问题
  • 已将解决方案更新至 VS2019 (community)
© . All rights reserved.