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 操作。 必须通过调用 CreateIoCompletionPort () 来启用 I/O 完成端口机制,将函数传递给 OVERLAPPED 结构的实例和以前与 I/O 完成端口关联的文件句柄:
- AcceptEx
- ConnectNamedPipe
- DeviceIoControl
- LockFileEx
- ReadDirectoryChangesW
- ReadFile
- TransactNamedPipe
- WaitCommEvent
- WriteFile
- WSASendMsg
- WSASendTo
- WSASend
- WSARecvFrom
- LPFN_WSARECVMSG (WSARecvMsg)
- WSARecv
相关主题