参数“NumberOfConcurrentThreads”如何在“CreateIoCompletionPort”中使用

How the parameter `NumberOfConcurrentThreads` is used in `CreateIoCompletionPort`

本文关键字:CreateIoCompletionPort NumberOfConcurrentThreads 参数      更新时间:2023-10-16

查看CreateIoCompletionPort的MSDN文档:

NumberOfConcurrentThreads[在]

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

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

然而,我找到的文档没有指出IoCompletionPort实际上创建任何线程。

实际上,Microsoft提供的示例使用以下代码来确定有多少处理内核可用(而不是将0传递给NumberOfConcurrentThreads),然后实际创建那么多线程。

// 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相关的"承载能力"。但这是如何体现的呢?我很难理解如何(甚至为什么这是可取的)一个访问完成端口的线程将被阻止从完成端口脱离队列。

实际上,我尝试修改创建线程为new HANDLE[++dwThreadCount]的行(并从声明中删除const说明符),并且该示例似乎没有任何问题。我刚注意到在执行结束时有一个额外的超时错误。

我目前唯一的结论是说NumberOfConcurrentThreads是一个没有实际用途的"虚拟"变量,所以我错过了什么?

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

I/O Completion Ports

I/O完成端口如何工作

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

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

线程和并发性

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

最有效的场景发生在队列中有完成数据包等待,但由于端口已达到并发限制,因此无法满足等待。考虑一个并发值和多个线程在GetQueuedCompletionStatus函数调用中等待时会发生什么。在这种情况下,如果队列总是有完成包等待,当正在运行的线程调用GetQueuedCompletionStatus时,它不会阻塞执行,因为正如前面提到的,线程队列是后进先出的。相反,这个线程将立即获取下一个排队的完成包。不会发生线程上下文切换,因为正在运行的线程不断地拾取完成包,而其他线程无法运行。

选择并发值的最佳最大值是计算机上的cpu数量。如果您的事务需要长时间的计算,那么较大的并发性值将允许更多的线程运行。每个完井包可能需要更长的时间来完成,但同时会处理更多的完井包。您可以将并发性值与分析工具结合起来进行实验,以实现应用程序的最佳效果。

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

IOCP基于KQUEUE对象:

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

MaximumCount如果被KeInitializeQueue (Count == 0)初始化,则由KeNumberProcessors赋值;如果从CreateIoCompletionPort (ZwCreateIoCompletion)初始化,则由NumberOfConcurrentThreads赋值。

CurrentCount是绑定到KQUEUE(在ETHREAD结构中有一个特殊的字段:KQUEUE* Queue)的"活动"(未等待)线程的数量。

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

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

如果一个线程向IOCP插入一个新的数据包,而其他线程之前正在等待,只有当(CurrentCount < MaximumCount)时,一个线程才会被唤醒(以后进先出顺序)。

当:

  • 一个线程开始等待某个对象(通过KeWaitForObject)
  • 线程被挂起(这也是内部调用KeWaitForObject)
  • KeDelayExecution (Sleep)被称为

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

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

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

相关文章:
  • 没有找到相关文章