同步和异步 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 结构的指针传递到对 ReadFile 和 WriteFile 的调用中。 执行同步 I/O 时,对 ReadFile 和 WriteFile 的调用不需要此结构。
注意
如果为异步 I/O 打开文件或设备,则使用该句柄对 WriteFile 等函数的后续调用通常会立即返回,但对于被阻止的执行,也可以同步执行。 有关详细信息,请参阅 https://support.microsoft.com/kb/156932。
尽管 CreateFile 是用于打开文件、磁盘卷、匿名管道和其他类似设备的最常见函数,但也可以使用来自其他系统对象(如套接字创建的套接字或接受函数)的句柄类型广播来执行 I/O 操作。
通过使用 FILE_FLAG_BACKUP_SEMANTICS 属性调用 CreateFile 函数来获取目录对象的句柄。 目录句柄几乎永远不会使用 — 备份应用程序是通常使用这些句柄的几个应用程序之一。
打开异步 I/O 的文件对象后,必须正确创建、初始化 并 传递到对 ReadFile 和 WriteFile 等函数的每个调用中。 在异步读取和写入操作中使用 OVERLAPPED 结构时,请记住以下事项:
- 在完成文件对象的所有异步 I/O 操作之前,请勿解除分配或修改 OVERLAPPED 结构或数据缓冲区。
- 如果将指向 OVERLAPPED 结构的指针声明为局部变量,则在完成文件对象的所有异步 I/O 操作之前,不要退出本地函数。 如果本地函数过早退出, 则 OVERLAPPED 结构将超出范围,并且无法访问它遇到的任何 ReadFile 或 WriteFile 函数。
还可以创建事件并将句柄置于 OVERLAPPED 结构中;然后,可以使用 等待函数 等待 I/O 操作完成,方法是等待事件句柄。
如前所述,在使用异步句柄时,应用程序在确定何时释放与该句柄上的指定 I/O 操作关联的资源时,应谨慎使用。 如果句柄过早解除分配, ReadFile 或 WriteFile 可能会错误地报告 I/O 操作已完成。 此外,WriteFile 函数有时会使用 getLastError 值ERROR_SUCCESS返回 TRUE,即使它使用的是异步句柄 (,它还可以使用ERROR_IO_PENDING) 返回 FALSE。 习惯于同步 I/O 设计的程序员通常此时会释放数据缓冲区资源,因为 TRUE 和 ERROR_SUCCESS 表示操作已完成。 但是,如果 I/O 完成端口 用于此异步句柄,即使 I/O 操作立即完成,也会发送完成数据包。 换句话说,如果应用程序在 WriteFile 返回 TRUE 后释放资源,除了 I/O 完成端口例程中 ERROR_SUCCESS 外,还会有一个双无错误条件。 在此示例中,建议允许完成端口例程完全负责此类资源的所有释放操作。
系统不会在异步句柄上维护文件指针,而支持文件指针的文件指针 (,即查找设备) ,因此必须将文件位置传递给 重叠 结构相关偏移数据成员中的读取和写入函数。 有关详细信息,请参阅 WriteFile 和 ReadFile。
同步句柄的文件指针位置由系统维护,因为数据是读取或写入的,也可以使用 SetFilePointer 或 SetFilePointerEx 函数进行更新。
应用程序还可以等待文件句柄来同步 I/O 操作的完成,但这样做需要极其谨慎。 每次启动 I/O 操作时,操作系统都会将文件句柄设置为非对齐状态。 每次完成 I/O 操作时,操作系统都会将文件句柄设置为信号状态。 因此,如果应用程序启动两个 I/O 操作并等待文件句柄,则无法确定当句柄设置为信号状态时完成的操作。 如果应用程序必须在单个文件上执行多个异步 I/O 操作,则应在每个 I/O 操作的特定 OVERLAPPED 结构中等待事件句柄,而不是在通用文件句柄上等待。
若要取消所有挂起的异步 I/O 操作,请使用以下任一操作:
- CancelIo - 此函数仅取消由指定文件句柄的调用线程发出的操作。
- CancelIoEx - 此函数取消指定文件句柄的线程发出的所有操作。
使用 CancelSynchronousIo 取消挂起的同步 I/O 操作。
ReadFileEx 和 WriteFileEx 函数使应用程序能够在异步 I/O 请求完成后指定执行 (查看 FileIOCompletionRoutine) 例程。