同步和异步 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 错误。 (例如,管道从另一端关闭。)
- 调用本身中出错(例如,一个或多个参数无效)。
- 进程中的另一个线程使用被阻止线程的线程句柄(终止该线程的 I/O)调用 CancelSynchronousIo 函数,导致 I/O 操作失败。
- 被阻止的线程由系统终止;例如,进程本身被终止,或者另一个线程使用被阻止线程的句柄调用 TerminateThread 函数。 (这通常被认为是最后的手段,而不是良好的应用程序设计。)
在某些情况下,这种延迟可能不符合应用程序的设计和目的,因此应用程序设计人员应考虑使用具有适当线程同步对象的异步 I/O,如 I/O 完成端口。 有关线程同步的详细信息,请参阅关于同步。
通过在 dwFlagsAndAttributes 参数中指定 FILE_FLAG_OVERLAPPED 标志,进程在调用 CreateFile 时打开一个用于异步 I/O 的文件。 如果未指定 FILE_FLAG_OVERLAPPED,则会为同步 I/O 打开该文件。 为异步 I/O 打开文件后,会将指向 OVERLAPPED 结构的指针传递到对 ReadFile 和 WriteFile 的调用中。 执行同步 I/O 时,在调用 ReadFile 和 WriteFile 时不需要此结构。
注意
如果打开了文件或设备进行异步 I/O,则使用该句柄对 WriteFile 等函数的后续调用通常会立即返回,但也可以与被阻止的执行同步。 有关详细信息,请参阅异步磁盘 I/O 在 Windows 上显示为同步。
尽管 CreateFile 是用于打开文件、磁盘卷、匿名管道和其他类似设备的最常见函数,但也可以使用其他系统对象的句柄 typecast 来执行 I/O 操作,例如由 socket 或 accept 函数创建的套接字。
目录对象的句柄是通过调用具有 FILE_FLAG_BACKUP_SEMANTICS 属性的 CreateFile 函数获得的。 目录句柄几乎从未被使用过,备份应用程序是少数几个通常会使用它们的应用程序之一。
打开文件对象进行异步 I/O 后,必须正确创建、初始化 OVERLAPPED 结构,并将其传递给每个函数调用,如 ReadFile 和 WriteFile。 在异步读取和写入操作中使用 OVERLAPPED 结构时,请记住以下几点:
- 在完成对文件对象的所有异步 I/O 操作之前,不要解除分配或修改 OVERLAPPED 结构或数据缓冲区。
- 如果将指向 OVERLAPPED 结构的指针声明为局部变量,则在完成对文件对象的所有异步 I/O 操作之前,不要退出本地函数。 如果过早退出本地函数,则 OVERLAPPED 结构将超出范围,并且在该函数之外遇到的任何 ReadFile 或 WriteFile 函数都无法访问它。
还可以创建一个事件,并将句柄放入 OVERLAPPED 结构中;然后,可以使用等待函数通过等待事件句柄来等待 I/O 操作完成。
如前所述,在使用异步句柄时,应用程序在确定何时释放与该句柄上的指定 I/O 操作关联的资源时,应格外谨慎。 如果句柄过早解除分配,ReadFile 或 WriteFile 可能会错误地报告 I/O 操作已完成。 此外,WriteFile 函数有时会返回 TRUE,其中 GetLastError 值为 ERROR_SUCCESS,即使它使用的是异步句柄(也可以在 ERROR_IO_PENDING 时返回 FALSE)。 习惯于同步 I/O 设计的程序员通常此时会释放数据缓冲区资源,因为 TRUE 和 ERROR_SUCCESS 表示操作已完成。 但是,如果此异步句柄使用 I/O 完成端口,则即使 I/O 操作立即完成,也会发送完成数据包。 换句话说,如果在 I/O 完成端口例程中,应用程序在 WriteFile 返回 TRUE 并返回 ERROR_SUCCESS 后释放资源,则它将出现 double-free 错误条件。 在此示例中,建议允许完成端口例程全权负责此类资源的所有释放操作。
系统不会在支持文件指针的文件和设备(即查找设备)的异步句柄上维护文件指针,因此必须将文件位置传递给 OVERLAPPED 结构的相关偏移数据成员中的读写函数。 有关详细信息,请参阅 WriteFile 和 ReadFile。
同步句柄的文件指针位置由系统在读取或写入数据时维护,也可以使用 SetFilePointer or SetFilePointerEx 函数进行更新。
应用程序还可以等待文件句柄来同步 I/O 操作的完成,但这样做需要格外小心。 每次启动 I/O 操作时,操作系统都会将文件句柄设置为非终止状态。 每次 I/O 操作完成时,操作系统都会将文件句柄设置为已发出信号状态。 因此,如果应用程序启动两个 I/O 操作并等待文件句柄,则当句柄设置为已发出信号状态时,无法确定哪个操作已完成。 如果应用程序必须对单个文件执行多个异步 I/O 操作,则它应该等待每个 I/O 操作的特定 OVERLAPPED 结构中的事件句柄,而不是公用文件句柄。
若要取消所有挂起的异步 I/O 操作,请使用以下任一方法:
- CancelIo - 此函数仅取消由指定文件句柄的调用线程发出的操作。
- CancelIoEx - 此函数取消由指定文件句柄的线程发出的所有操作。
使用 CancelSynchronousIo 取消挂起的同步 I/O 操作。
当异步 I/O 请求完成时,ReadFileEx 和 WriteFileEx 函数使应用程序能够指定要执行的例程(请参阅 FileIOCompletionRoutine)。