同步和异步 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 请求预计需要大量时间(例如大型数据库的刷新或备份或通信链接缓慢)的情况下,异步 I/O 通常是优化处理效率的好方法。 但是,对于相对快速的 I/O 操作,处理内核 I/O 请求和内核信号的开销可能会使异步 I/O 不那么有益,特别是在需要进行许多快速 I/O 操作的情况下。 在这种情况下,同步 I/O 会更好。 如何完成这些任务的机制和实现详细信息因所使用的设备句柄类型和应用程序的特定需求而异。 换句话说,解决这个问题通常有多种方法。

同步和异步 I/O 注意事项

如果为同步 I/O 打开文件或设备(即未指定 FILE_FLAG_OVERLAPPED ),则 对 WriteFile 等函数的后续调用可能会阻止调用线程的执行,直到发生以下事件之一:

  • I/O 操作完成(在本例中为数据写入)。
  • 出现 I/O 错误。 (例如,管道从另一端关闭。)
  • 调用本身中出错(例如,一个或多个参数无效)。
  • 进程中的另一个线程使用阻塞线程的线程句柄调用 CancelSynchronousIo 函数,该函数终止该线程的 I/O 操作,从而导致 I/O 操作失败。
  • 被阻止的线程由系统终止;例如,进程本身被终止,或者另一个线程使用阻塞线程的句柄调用 TerminateThread 函数。 (这通常被认为是最后的手段,而不是良好的应用程序设计。)

在某些情况下,这种延迟可能不符合应用程序的设计和目的,因此应用程序设计人员应考虑使用具有适当线程同步对象的异步 I/O,如 I/O 完成端口。 有关线程同步的详细信息,请参阅关于同步

进程通过在 dwFlagsAndAttributes 参数中指定FILE_FLAG_OVERLAPPED标志,在对 CreateFile 的调用中打开一个用于异步 I/O 的文件。 如果未指定 FILE_FLAG_OVERLAPPED,则会为同步 I/O 打开该文件。 为异步 I/O 打开文件后,会将指向 OVERLAPPED 结构的指针传递到对 ReadFileWriteFile 的调用中。 执行同步 I/O 时,在调用 ReadFileWriteFile 时不需要此结构。

注意

如果打开了文件或设备以进行异步 I/O,则使用该句柄对 WriteFile 等函数的后续调用通常会立即返回,但在需要的情况下,也可能会与阻塞执行同步运行。 有关详细信息,请参阅异步磁盘 I/O 在 Windows 上显示为同步

尽管 CreateFile 是最常用来打开文件、磁盘卷、匿名管道以及其他类似设备的函数,但也可以通过将其他系统对象(如通过socketaccept函数创建的套接字)进行类型转换来获取句柄,以执行 I/O 操作。

要获取目录对象的句柄,需要通过调用带有 FILE_FLAG_BACKUP_SEMANTICS 属性的 CreateFile 函数。 目录句柄几乎从未被使用过,备份应用程序是少数几个通常会使用它们的应用程序之一。

打开异步 I/O 的文件对象后,必须正确创建、初始化并传入对 ReadFileWriteFile 等函数的每个调用中的 OVERLAPPED 结构。 在异步读取和写入作中使用 OVERLAPPED 结构时,请记住以下几点:

  • 在文件对象的所有异步 I/O 操作完成之前,请勿解除分配或修改 OVERLAPPED 结构或数据缓冲区。
  • 如果将指向 OVERLAPPED 结构的指针声明为局部变量,则在完成对文件对象的所有异步 I/O作之前,不要退出本地函数。 如果本地函数过早退出,则 OVERLAPPED 结构将在作用域外,并且在该函数之外将无法访问其遇到的任何 ReadFileWriteFile 函数。

还可以创建事件并将句柄置于 OVERLAPPED 结构中;然后,可以使用 等待函数 通过等待事件句柄来等待 I/O 操作的完成。

如前所述,在使用异步句柄时,应用程序在确定何时释放与该句柄上的指定 I/O 操作关联的资源时,应格外谨慎。 如果句柄过早解除分配,ReadFileWriteFile 可能会错误地报告 I/O 操作已完成。 此外,WriteFile 函数有时会返回 TRUE,其 GetLastError 值为 ERROR_SUCCESS,即使它使用的是异步句柄(也可以使用 ERROR_IO_PENDING 返回 FALSE)。 习惯于同步 I/O 设计的程序员通常此时会释放数据缓冲区资源,因为 TRUEERROR_SUCCESS 表示操作已完成。 但是,如果此异步句柄使用 I/O 完成端口,则即使 I/O 操作立即完成,也会发送完成数据包。 换句话说,如果在 I/O 完成端口例程中,应用程序在 WriteFile 返回 TRUE 并返回 ERROR_SUCCESS 后释放资源,则它将出现 double-free 错误条件。 在此示例中,建议允许完成端口例程全权负责此类资源的所有释放操作。

系统不会在异步句柄上维护文件指针,这些文件和设备支持文件指针(即查找设备),因此必须将文件位置传递给 OVERLAPPED 结构相关偏移数据成员中的读取和写入函数。 有关详细信息,请参阅 WriteFileReadFile

同步句柄的文件指针位置由系统维护,在数据读取或写入时可以使用 SetFilePointerSetFilePointerEx 函数进行更新。

应用程序还可以等待文件句柄来同步 I/O 操作的完成,但这样做需要格外小心。 每次启动 I/O 操作时,操作系统都会将文件句柄设置为非终止状态。 每次 I/O 操作完成时,操作系统都会将文件句柄设置为已发出信号状态。 因此,如果应用程序启动两个 I/O 操作并等待文件句柄,则当句柄设置为已发出信号状态时,无法确定哪个操作已完成。 如果应用程序必须在单个文件上执行多个异步 I/O 操作,那么应当在每个 I/O 操作的特定 OVERLAPPED 结构中的事件句柄上等待,而不是在通用文件句柄上等待。

取消 I/O 操作

若要取消所有挂起的异步 I/O 操作,请使用以下任一方法:

  • CancelIo:此函数仅取消调用线程为指定文件句柄发出的操作。
  • CancelIoEx:此函数取消针对指定文件句柄由线程发出的所有操作。

使用 CancelSynchronousIo 取消挂起的同步 I/O 操作。

ReadFileExWriteFileEx 函数使应用程序能够在异步 I/O 请求完成后指定要执行的例程(请参阅 FileIOCompletionRoutine)。

写入文件

读取文件