IOCP线程 - 澄清?

Roy*_*mir 12 c# iocp async-await

阅读本文后指出:

设备完成其作业后(IO操作) - 它通过中断通知CPU.

...... ......

但是,"完成"状态仅存在于操作系统级别; 该进程有自己的内存空间,必须通知

...... ......

由于库/ BCL使用标准的P/Invoke重叠I/O系统,因此它已经使用I/O完成端口(IOCP)注册了句柄,IOCP是线程池的一部分.

...... ......

因此,简单地借用 I/O线程池线程来执行APC,APC通知任务它已完成.

关于大胆的部分,我很有趣:

如果我理解正确,在IO操作完成后,它必须通知执行IO操作的实际进程.

问题#1:

这是否意味着它为每个完成的IO操作抓取一个新的线程池线程?或者这是一个专门的线程数?

问题2:

看着 :

for (int i=0;i<1000;i++)
    {
      PingAsync_NOT_AWAITED(i); //notice not awaited !
    }
Run Code Online (Sandbox Code Playgroud)

这是否意味着我将同时运行1000个IOCP线程池线程(某种类型),当它们全部完成时?

Jer*_*ert 14

这是否意味着它为每个完成的IO操作抓取一个新的线程池线程?或者这是一个专门的线程数?

为每个I/O请求创建一个新线程,以达到目的,这将是非常低效的.相反,运行时以少量线程(具体数量取决于您的环境)开始,并根据需要添加和删除工作线程(对此的确切算法同样因您的环境而异)..NET的主要版本已经看到了这个实现的变化,但基本思想保持不变:运行时尽力创建和维护尽可能多的线程,以便有效地为所有I/O提供服务.在我的系统(Windows 8.1,.NET 4.5.2)上,一个全新的控制台应用程序在进程中只有3个线程Main,并且在请求实际工作之前这个数字不会增加.

这是否意味着我将同时运行1000个IOCP线程池线程(某种类型),当它们全部完成时?

不会.当您发出I/O请求时,线程将在完成端口上等待以获取结果并调用已注册的任何回调来处理结果(无论是通过BeginXXX方法还是作为任务的继续).如果您使用任务而不等待它,那么该任务就会在那里结束并且该线程将返回到线程池.

如果你等了它怎么办?1000个I/O请求的结果不会真正同时到达,因为中断并非全部同时到达,但是假设间隔比我们处理它们所需的时间短得多.在这种情况下,线程池将继续启动线程以处理结果,直到达到最大值,并且任何进一步的请求将最终在完成端口上排队.根据您的配置方式,这些线程可能需要一些时间才能启动.

考虑以下(故意可怕)玩具程序:

static void Main(string[] args) {
    printThreadCounts();
    var buffer = new byte[1024];
    const int requestCount = 30;
    int pendingRequestCount = requestCount;
    for (int i = 0; i != requestCount; ++i) {
        var stream = new FileStream(
            @"C:\Windows\win.ini",
            FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 
            buffer.Length, FileOptions.Asynchronous
        );
        stream.BeginRead(
            buffer, 0, buffer.Length,
            delegate {
                Interlocked.Decrement(ref pendingRequestCount);
                Thread.Sleep(Timeout.Infinite);
            }, null
        );
    }
    do {
        printThreadCounts();
        Thread.Sleep(1000);
    } while (Thread.VolatileRead(ref pendingRequestCount) != 0);
    Console.WriteLine(new String('=', 40));
    printThreadCounts();
}

private static void printThreadCounts() {
    int completionPortThreads, maxCompletionPortThreads;
    int workerThreads, maxWorkerThreads;
    ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);
    ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine(
        "Worker threads: {0}, Completion port threads: {1}, Total threads: {2}", 
        maxWorkerThreads - workerThreads, 
        maxCompletionPortThreads - completionPortThreads, 
        Process.GetCurrentProcess().Threads.Count
    );
}
Run Code Online (Sandbox Code Playgroud)

在我的系统(具有8个逻辑处理器)上,输出如下(结果可能因系统而异):

Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 8, Total threads: 12
Worker threads: 0, Completion port threads: 9, Total threads: 13
Worker threads: 0, Completion port threads: 11, Total threads: 15
Worker threads: 0, Completion port threads: 13, Total threads: 17
Worker threads: 0, Completion port threads: 15, Total threads: 19
Worker threads: 0, Completion port threads: 17, Total threads: 21
Worker threads: 0, Completion port threads: 19, Total threads: 23
Worker threads: 0, Completion port threads: 21, Total threads: 25
Worker threads: 0, Completion port threads: 23, Total threads: 27
Worker threads: 0, Completion port threads: 25, Total threads: 29
Worker threads: 0, Completion port threads: 27, Total threads: 31
Worker threads: 0, Completion port threads: 29, Total threads: 33
========================================
Worker threads: 0, Completion port threads: 30, Total threads: 34
Run Code Online (Sandbox Code Playgroud)

当我们发出30个异步请求时,线程池会快速生成8个线程来处理结果,但之后它只会以每秒约2个的悠闲速度旋转新线程.这表明如果要正确利用系统资源,最好确保快速完成I/O处理.实际上,让我们将代理改为以下代表,代表请求的"正确"处理:

stream.BeginRead(
    buffer, 0, buffer.Length,
    ar => {
        stream.EndRead(ar);
        Interlocked.Decrement(ref pendingRequestCount);
    }, null
);
Run Code Online (Sandbox Code Playgroud)

结果:

Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 1, Total threads: 11
========================================
Worker threads: 0, Completion port threads: 0, Total threads: 11
Run Code Online (Sandbox Code Playgroud)

同样,结果可能会因系统和运行而异.在这里,我们几乎没有看到正在运行的完成端口线程,而我们发出的30个请求在没有启动新线程的情况下完成.您应该发现可以将"30"更改为"100"甚至"100000":我们的循环无法比完成请求更快地启动请求.但请注意,结果偏向于我们的偏好,因为"I/O"反复读取相同的字节,并且将从操作系统缓存中提供服务,而不是从磁盘读取.这并不意味着展示实际的吞吐量,当然,仅仅是开销的差异.

要使用工作线程而不是完成端口线程重复这些结果,只需更改FileOptions.AsynchronousFileOptions.None.这使文件访问同步,异步操作将在工作线程上完成,而不是使用完成端口:

Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 8, Completion port threads: 0, Total threads: 15
Worker threads: 9, Completion port threads: 0, Total threads: 16
Worker threads: 10, Completion port threads: 0, Total threads: 17
Worker threads: 11, Completion port threads: 0, Total threads: 18
Worker threads: 12, Completion port threads: 0, Total threads: 19
Worker threads: 13, Completion port threads: 0, Total threads: 20
Worker threads: 14, Completion port threads: 0, Total threads: 21
Worker threads: 15, Completion port threads: 0, Total threads: 22
Worker threads: 16, Completion port threads: 0, Total threads: 23
Worker threads: 17, Completion port threads: 0, Total threads: 24
Worker threads: 18, Completion port threads: 0, Total threads: 25
Worker threads: 19, Completion port threads: 0, Total threads: 26
Worker threads: 20, Completion port threads: 0, Total threads: 27
Worker threads: 21, Completion port threads: 0, Total threads: 28
Worker threads: 22, Completion port threads: 0, Total threads: 29
Worker threads: 23, Completion port threads: 0, Total threads: 30
Worker threads: 24, Completion port threads: 0, Total threads: 31
Worker threads: 25, Completion port threads: 0, Total threads: 32
Worker threads: 26, Completion port threads: 0, Total threads: 33
Worker threads: 27, Completion port threads: 0, Total threads: 34
Worker threads: 28, Completion port threads: 0, Total threads: 35
Worker threads: 29, Completion port threads: 0, Total threads: 36
========================================
Worker threads: 30, Completion port threads: 0, Total threads: 37
Run Code Online (Sandbox Code Playgroud)

线程池每秒旋转一个工作线程,而不是它为完成端口线程启动的两个工作线程.显然这些数字是依赖于实现的,并且可能在新版本中发生变化.

最后,让我们演示如何使用ThreadPool.SetMinThreads确保最少数量的线程可用于完成请求.如果我们回到FileOptions.Asynchronous并添加ThreadPool.SetMinThreads(50, 50)Main我们的玩具程序中,结果是:

Worker threads: 0, Completion port threads: 0, Total threads: 3
Worker threads: 0, Completion port threads: 31, Total threads: 35
========================================
Worker threads: 0, Completion port threads: 30, Total threads: 35
Run Code Online (Sandbox Code Playgroud)

现在,线程池不是每两秒耐心地添加一个线程,而是继续旋转线程直到达到最大值(在这种情况下不会发生,因此最终计数保持在30).当然,所有这30个线程都处于无限等待状态 - 但如果这是一个真正的系统,那么这30个线程现在可能会在非常有效的工作中发挥作用.我不会尝试这个 100000名的请求,虽然.


Lua*_*aan 10

这有点宽泛,所以我只想谈谈要点:

IOCP线程位于单独的线程池中,可以这么说 - 这就是I/O线程设置.因此,它们不会与用户线程池线程冲突(就像您在正常await操作中所拥有的那样ThreadPool.QueueWorkerItem).

就像普通的线程池一样,它只会随着时间的推移缓慢地分配新的线程.因此,即使异步响应的峰值同时发生,您也不会拥有1000个I/O线程.

在一个正确的异步应用程序中,你不会拥有超过核心数量,给予或接受,就像工作线程一样.那是因为你要么正在做大量的CPU工作,要么将它发布在正常的工作线程上,要么你正在进行I/O工作,你应该将其作为异步操作.

我的想法是你在I/O回调中花费的时间很少 - 你不会阻塞,并且你不会做很多CPU工作.如果你违反这个(比如,添加Thread.Sleep(10000)到你的回调中),那么是的,.NET将随着时间的推移创建大量的IO线程 - 但这只是不正确的使用.

现在,I/O线程与普通CPU线程有何不同?它们几乎是相同的,它们只是等待一个不同的信号 - 两者都是(简化警报)只是while一个方法的循环,当一个新的工作项被应用程序的某个其他部分(或操作系统)排队时,该方法可以提供控制.主要区别在于I/O线程正在使用IOCP队列(OS托管),而普通工作线程有自己的队列,完全由.NET管理并且可由应用程序员访问.

作为旁注,请不要忘记您的请求可能已同步完成.也许你是在while循环中读取TCP流,一次512字节.如果套接字缓冲区中有足够的数据,则多个ReadAsyncs可以立即返回,而根本不进行任何线程切换.这通常不是问题,因为I/O往往是您在典型应用程序中执行的最耗时的事情,因此不必等待I/O通常很好.但是,根据某些部分异步发生的错误代码(即使不保证)可能很容易破坏您的应用程序.


Sri*_*vel 5

这是否意味着我将同时运行1000个IOCP线程池线程(某种类型),当它们全部完成时?

一点都不.与可用的工作线程相同,ThreadPool我们也有"完成端口线程".

这些线程专用于异步I/O. 不会预先创建线程.它们是按需创建的,与工作线程相同.当线程池决定时,它们最终会被销毁.

通过简单借用作者意味着通知完成IO的过程,使用来自"完成端口线程"(ThreadPool)的任意线程.它不会执行任何冗长的操作,而是完成IO通知.

  • @RoyiNamir它在某个缓冲区某处.有很多层缓冲,所以要说准确的地方并不容易.然而,当你收到通知时,它已经必须在*你的*缓冲区 - 当然,如果你使用的是像'HttpClient`这样的东西,它就是他的缓冲区,而如果你直接使用例如`TcpClient`,它就是当你执行`ReceiveAsync`时,你给它的`byte []`缓冲区.当然,这是你想要使用最高可用抽象的原因之一 - 网络(和任何异步性)很难,让聪明人处理最难的部分:D (4认同)