IOCP是在I/O进行时还是之后运行的线程?

9
我正在尝试理解I/O完成端口及其与使用async-await进行I/O操作的关系。
臭名昭著的文章“没有线程”谈到了在I/O完成后临时借用IOCPs的问题。因为整篇文章的重点在于展示当高级硬件层面的I/O操作正在执行时,没有线程会被循环占用,如下所示:

I/O完成了吗?不是。 I/O完成了吗?不是。 I/O完成了吗?不是。...

但是我看到这篇文章中提到一个组件负责检查队列中的完成端口,并给出了以下示例:
public class IOCompletionWorker
{ 
    public unsafe void Start(IntPtr completionPort)
    {
        while (true)
        {
            uint bytesRead;
            uint completionKey;
            NativeOverlapped* nativeOverlapped;

            var result = Interop.GetQueuedCompletionStatus(
                completionPort, 
                out bytesRead,
                out completionKey,
                &nativeOverlapped, 
                uint.MaxValue);

            var overlapped = Overlapped.Unpack(nativeOverlapped);

            if (result)
            {
                var asyncResult = ((FileReadAsyncResult)overlapped.AsyncResult);
                asyncResult.ReadCallback(bytesRead, asyncResult.Buffer);
            }
            else
            {
                ThreadLogger.Log(Interop.GetLastError().ToString());
            }

            Overlapped.Free(nativeOverlapped);
        }
    }
}

var completionPortThread = new Thread(() => new IOCompletionWorker().Start(completionPortHandle))
{
    IsBackground = true
};
completionPortThread.Start();

在我看来,似乎正在进行一些轮询。

我想我的问题可以归结为:

  • .NET应用程序是否有两种类型的线程池 -- (1)“工作线程”和(2)“I/O线程”?
  • 如果是这样,是否有一个固定的数量,在配置中指定,例如M个工作线程和N个I/O线程?通常M与N的比例是多少?
  • I/O线程确切地在何时使用?

你的问题得到解答了吗?:) - fbrosseau
1个回答

11

两篇文章各有其道理。

IOCP并不是线程。它们可以被看作是某种队列,内核(或者也可以通过PostQueuedCompletionStatus的常规用户模式代码)可以在其中发布完成项。IOCP本身没有固有的线程模型或关联的线程,它们只是多生产者-消费者队列。

以网络套接字为例,但任何类型的异步工作都是如此:

  • 您在绑定到IOCP的重叠模式套接字上调用WSARecv,由网络驱动程序执行必要的操作以设置实际的数据接收请求。没有活跃的线程等待您的数据到达。
  • 数据到达。硬件唤醒操作系统。操作系统将在内核中为网络驱动程序提供一些CPU时间来处理传入事件。网络驱动程序处理中断,然后因为您的套接字绑定到了IOCP,会向您的IOCP队列发布完成项。请求已完成。

在这些操作中,实际上没有涉及来自您进程的用户模式线程(除了最初的异步调用)。如果您想对数据到达这一事实进行操作(我假设您读取套接字时会这样做!),那么您必须从IOCP中出队完成的项。

IOCP的重点在于,您可以将数千个IO句柄(套接字、文件等)绑定到单个IOCP上。然后,您可以使用一个线程来驱动这些数千个异步进程并行运行。

是的,执行GetQueuedCompletionStatus的那个线程在没有IOCP挂起完成时会被阻塞,所以这可能就是您感到困惑的地方。但是IOCP的要点在于,当您可以拥有数十万个网络操作在任何给定时间都处于挂起状态时,您就可以阻塞该线程,而所有服务都由一个线程提供。您永远不会对IO句柄/IOCP/服务线程进行1:1:1映射,因为那样您将失去任何异步的好处,您也可能只使用同步IO。

IOCP主要的目标是在Windows下实现令人印象深刻的异步操作并行性。

我希望这能澄清混乱。

至于具体问题:

  1. 是的,.Net框架有两个线程池。一个是纯粹用于用户模式通用工作的“Worker”线程池。另一个是“IO”线程池。第二个是为了在编写高级C#代码时隐藏所有IOCP管理,并使您的异步套接字就像魔术一样工作。
  2. 这都是可以随时更改的实现细节,但答案是两个线程池都是独立的。如果您在worker线程池上有大量工作带宽并且框架决定通过添加新线程来增加整体吞吐量,它将仅向worker线程池添加线程,并且不会触及IO线程池。同样适用于IO线程池,如果您有阻塞IO线程的行为异常代码,则会生成新的IO线程池而不会触及worker线程池。您可以使用ThreadPool.SetMinThreads/SetMaxThreads自定义数字,但这通常是进程错误使用线程池的迹象。
  3. 当从线程池的内部IOCP出队项目时,将使用IO线程。在典型代码中,这将在某个IO句柄上完成异步操作时发生。您还可以通过UnsafeQueueNativeOverlapped自己排队项目,但这不太常见。

纯托管的异步操作(例如使用Task.Delay进行async-await)不涉及任何IO句柄,因此它们不会被驱动程序发布到IOCP,并且这些操作将属于“Worker”类别。

顺便说一句,您可以通过调用堆栈来区分Worker线程和IO线程。 Worker线程将以“ThreadPoolWorkQueue.Dispatch”开始其托管调用堆栈,而IO线程将以“_IOCompletionCallback.PerformIOCompletionCallback”开始其托管调用堆栈。这都是可以随时更改的实现细节,但在调试托管代码时了解正在处理的内容可能很有帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接