I/O 完成端口

I/O 完成端口提供了一个高效的线程模型,用于处理多处理器系统上的多个异步 I/O 请求。 当进程创建 I/O 完成端口时,系统会为线程创建关联的队列对象,该对象的唯一目的是为这些请求提供服务。 与在收到 I/O 请求时创建线程相比,处理多个并发异步 I/O 请求的进程可以通过将 I/O 完成端口与预分配的线程池结合使用来更快、更高效地执行此操作。

I/O 完成端口的工作原理

CreateIoCompletionPort 函数创建 I/O 完成端口,并将一个或多个文件句柄与该端口相关联。 当其中一个文件句柄上的异步 I/O 操作完成时,I/O 完成数据包将排入先进先出 (FIFO) 相关 I/O 完成端口的顺序。 此机制的一个强大用途是将多个文件句柄的同步点合并到单个对象中,尽管还有其他有用的应用程序。 请注意,当数据包按 FIFO 顺序排队时,它们可能会以不同的顺序取消排队。

注意

此处使用的术语 “文件句柄 ”是指表示重叠 I/O 终结点的系统抽象,而不仅仅是磁盘上的文件。 例如,它可以是网络终结点、TCP 套接字、命名管道或邮件槽。 可以使用支持重叠 I/O 的任何系统对象。 有关相关 I/O 函数的列表,请参阅本主题末尾。

 

当文件句柄与完成端口关联时,传入的状态块不会更新,直到从完成端口中删除数据包。 唯一的例外是原始操作以同步方式返回并返回错误。 线程 (main线程创建的线程或main线程本身) 使用 GetQueuedCompletionStatus 函数等待完成数据包排队到 I/O 完成端口,而不是直接等待异步 I/O 完成。 阻止其在 I/O 完成端口上执行的线程按后进先出 (LIFO) 顺序释放,并从该线程的 I/O 完成端口的 FIFO 队列中拉取下一个完成数据包。 这意味着,当完成数据包发布到线程时,系统会释放与该端口关联的最后 (最近的) 线程,并向其传递最早的 I/O 完成的完成信息。

尽管任意数量的线程都可以为指定的 I/O 完成端口调用 GetQueuedCompletionStatus ,但当指定线程首次调用 GetQueuedCompletionStatus 时,它会与指定的 I/O 完成端口相关联,直到发生以下三种情况之一:线程退出、指定不同的 I/O 完成端口或关闭 I/O 完成端口。 换句话说,单个线程最多可以与一个 I/O 完成端口相关联。

当完成数据包排队到 I/O 完成端口时,系统首先检查与该端口关联的线程数正在运行。 如果正在运行的线程数小于) 下一部分 (讨论的并发值,则允许 (最近一个) 处理完成数据包的等待线程之一。 当正在运行的线程完成其处理时,它通常会再次调用 GetQueuedCompletionStatus ,此时它将返回下一个完成数据包,或者在队列为空时等待。

线程可以使用 PostQueuedCompletionStatus 函数将完成数据包放置在 I/O 完成端口的队列中。 这样,除了从 I/O 系统接收 I/O 完成数据包外,完成端口还可用于从进程的其他线程接收通信。 PostQueuedCompletionStatus 函数允许应用程序将自己的特殊用途完成数据包排队到 I/O 完成端口,而无需启动异步 I/O 操作。 例如,这对于通知工作线程外部事件很有用。

I/O 完成端口句柄以及与该特定 I/O 完成端口关联的每个文件句柄称为 对 I/O 完成端口的引用。 当不再引用 I/O 完成端口时,将释放该端口。 因此,必须正确关闭所有这些句柄才能释放 I/O 完成端口及其关联的系统资源。 满足这些条件后,应用程序应通过调用 CloseHandle 函数关闭 I/O 完成端口句柄。

注意

I/O 完成端口与创建它的进程相关联,并且不能在进程之间共享。 但是,同一进程中的线程之间可共享单个句柄。

 

线程和并发

要仔细考虑的 I/O 完成端口的最重要属性是并发值。 完成端口的并发值是在通过 NumberOfConcurrentThreads 参数使用 CreateIoCompletionPort 创建时指定的。 此值限制与完成端口关联的可运行线程数。 当与完成端口关联的可运行线程总数达到并发值时,系统会阻止执行与该完成端口关联的任何后续线程,直到可运行线程数低于并发值。

当队列中有完成数据包等待,但由于端口已达到其并发限制,无法满足任何等待时,会出现最有效的方案。 考虑 在 GetQueuedCompletionStatus 函数调用中等待一个和多个线程的并发值会发生什么情况。 在这种情况下,如果队列始终等待完成数据包,当正在运行的线程调用 GetQueuedCompletionStatus 时,它不会阻止执行,因为如前所述,线程队列是 LIFO。 相反,此线程将立即选取下一个排队的完成数据包。 不会发生线程上下文切换,因为正在运行的线程不断拾取完成数据包,而其他线程无法运行。

注意

在前面的示例中,额外的线程似乎毫无用,并且永远不会运行,但这假定正在运行的线程永远不会被其他某种机制置于等待状态、终止或以其他方式关闭其关联的 I/O 完成端口。 设计应用程序时,请考虑所有此类线程执行的影响。

 

要为并发值选择的最佳总体最大值是计算机上的 CPU 数。 如果事务需要长时间计算,则较大的并发值将允许运行更多线程。 每个完成数据包可能需要更长的时间才能完成,但将同时处理更多完成数据包。 可以结合分析工具来试验并发值,以达到应用程序的最佳效果。

如果与同一 I/O 完成端口关联的另一个运行线程由于其他原因(例如 SuspendThread 函数)进入等待状态,系统还允许在 GetQueuedCompletionStatus 中等待的线程处理完成数据包。 当处于等待状态的线程再次开始运行时,活动线程数可能会有一小段时间超过并发值。 但是,在活动线程数低于并发值之前,系统不允许任何新的活动线程,从而快速减少此数量。 这是让应用程序在其线程池中创建比并发值更多的线程的原因之一。 线程池管理超出了本主题的范围,但一个很好的经验法则是线程池中的线程数至少是系统上处理器数的两倍。 有关线程池的其他信息,请参阅 线程池

支持的 I/O 函数

以下函数可用于启动使用 I/O 完成端口完成的 I/O 操作。 必须通过调用 CreateIoCompletionPort () 向函数传递 OVERLAPPED 结构的实例和先前与 I/O 完成端口关联的文件句柄,以启用 I/O 完成端口机制:

关于进程和线程

BindIoCompletionCallback

CreateIoCompletionPort

GetQueuedCompletionStatus

GetQueuedCompletionStatusEx

PostQueuedCompletionStatus