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 函数的列表,请参阅本主题的末尾。

 

当文件句柄与完成端口关联时,在从完成端口中删除数据包之前,传入的状态块将不会更新。 唯一的例外是原始作以同步方式返回并出现错误。 线程(主线程或主线程本身创建的线程)使用 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 完成端口的最重要属性是并发值。 使用 CreateIoCompletionPort 通过 NumberOfConcurrentThreads 参数创建完成端口的并发值时指定。 此值限制与完成端口关联的可运行线程数。 当与完成端口关联的可运行线程总数达到并发值时,系统会阻止执行与该完成端口关联的任何后续线程,直到可运行线程数下降到并发值以下。

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

注意

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

 

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

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

支持的 I/O 函数

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

关于进程和线程

BindIoCompletionCallback

CreateIoCompletionPort

GetQueuedCompletionStatus

GetQueuedCompletionStatusEx

PostQueuedCompletionStatus