在CreateIoCompletionPort中如何使用参数NumberOfConcurrentThreads

查看CreateIoCompletionPort的MSDN文档 ,我们阅读:

NumberOfConcurrentThreads [in]

操作系统可以允许并发处理I / O完成端口的I / O完成数据包的最大线程数。 如果ExistingCompletionPort参数不是NULL,则忽略此参数。

如果此参数为零,则系统允许与系统中的处理器一样多的并发运行的线程。

但是,我发现的文档没有提供IoCompletionPort实际上创build任何线程的迹象。

实际上,Microsoft提供的示例使用以下代码来确定有多less处理内核可用(而不是为NumberOfConcurrentThreads传入0 ),然后实际创build多个线程。

 // The general value of the thread count is the system's processor count. SYSTEM_INFO sysInfo = { 0 }; GetNativeSystemInfo(&sysInfo); const DWORD dwThreadCount = sysInfo.dwNumberOfProcessors; // A class in the example that wraps around IoCompletionPort IOCompletionPort port; // Construct the thread pool HANDLE* hThreads = new HANDLE[dwThreadCount]; for (DWORD i = 0; i < dwThreadCount; ++i) { // The threads run CompletionThread hThreads[i] = CreateThread(0, 0, IOCompletionThread, &port, 0, NULL); } 

这似乎表明,不知何故有一个与IoCompletionPort相关的“承载能力”。 但是这怎么performance出来呢? 我很难看到如何(甚至为什么这是可取的),一个可以访问完成端口的线程将被阻止从完成端口出队。

事实上,我尝试修改线程创build线程是new HANDLE[++dwThreadCount] (并从声明中删除const说明符),该示例似乎执行没有抱怨。 在执行结束时,我只注意到一个额外的超时错误。

我现在唯一的结论就是说NumberOfConcurrentThreads是一个没有实际用法的“虚拟”variables,所以我错过了什么?

IOCP基于KQUEUE对象 :

 struct KQUEUE { DISPATCHER_HEADER Header; LIST_ENTRY EntryListHead; ULONG CurrentCount; ULONG MaximumCount; LIST_ENTRY ThreadListHead; }; 

如果初始化由KeInitializeQueue (Count == 0)或NumberOfConcurrentThreads如果从CreateIoCompletionPort(ZwCreateIoCompletion)初始化),则MaximumCountKeNumberProcessors分配。

CurrentCount是绑定到这个KQUEUE的“活动”(不等待)线程的KQUEUE (在ETHREAD结构中有一个特殊的字段: KQUEUE* Queue )。

如果一个线程试图通过调用KeRemoveQueueZwRemoveIoCompletionGetQueuedCompletionStatus )从KQUEUE删除一个数据包,并且IOCP中没有数据包( EntryListHead为空),那么线程当然会进入等待状态。

但是,如果数据包存在,系统会查看CurrentCountMaximumCount 。 如果CurrentCount < MaximumCount ,数据包将被删除(并且CurrentCount++递增)。 否则,线程将进入等待状态。

如果一个线程插入一个新的数据包到其他线程先前正在等待的IOCP,只有当( CurrentCount < MaximumCount )时才会唤醒(按LIFO顺序)。

什么时候:

  • 一个线程开始等待一些对象(由KeWaitForObject
  • 线程暂停(这也是在内部调用KeWaitForObject
  • KeDelayExecution (睡眠)被调用

系统查看Thread->Queue ,如果不是0 ,则CurrentCount--递减。 此外,如果数据包存在于IOCP中,线程正在等待它,并且CurrentCount < MaximumCount那么一个线程将被唤醒。

所以这个逻辑实际上是相当复杂的,但是最重要的是不超过MaximumCount线程将能够一次处理来自IOCP的数据包。

通常情况下,最好的值是KeNumberProcessors ,但是(在一些特殊情况下)分析工具可能会帮助您决定一个更适合您的情况的不同值。

I / O完成端口本身不会创建线程。 NumberOfConcurrentThreads参数指定允许多少个线程同时并行处理完成数据包。 这在另一个MSDN页面上有更详细的解释:

I / O完成端口

I / O完成端口如何工作

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

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

线程和并发

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

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

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

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