async-await如何“保存线程”?

时间:2018-04-15 01:56:59

标签: c# .net multithreading asynchronous async-await

我理解使用无线程异步,有更多线程可用于服务输入(例如HTTP请求),但我不明白当异步操作完成并需要线程时,这不会导致导致线程不足的原因继续他们的继续。

假设我们只有3个帖子

Thread 1 | 
Thread 2 |
Thread 3 |

并且它们在需要线程的长时间运行操作中被阻止(例如,在单独的数据库服务器上进行数据库查询)

Thread 1 | --- | Start servicing request 1 | Long-running operation .................. |
Thread 2 | ------------ | Start servicing request 2 | Long-running operation ......... |
Thread 3 | ------------------- | Start servicing request 3 | Long-running operation ...|
               |
              request 1
                        |
                      request 2
                                |
                              request 3
                                               |
                                           request 4 - BOOM!!!!

使用async-await可以使其像

一样
Thread 1 | --- | Start servicing request 1 | --- | Start servicing request 4 | ----- |
Thread 2 | ------------ | Start servicing request 2 | ------------------------------ |
Thread 3 | ------------------- | Start servicing request 3 | ----------------------- |
               |
              request 1
                        |
                      request 2
                                |
                              request 3
                                                 |
                                           request 4 - OK   

然而,在我看来,这可能会导致“飞行中”的异步操作过剩,如果同时完成太多,则没有线程可用于继续运行。

Thread 1 | --- | Start servicing request 1 | --- | Start servicing request 4 | ----- |
Thread 2 | ------------ | Start servicing request 2 | ------------------------------ |
Thread 3 | ------------------- | Start servicing request 3 | ----------------------- |
               |
              request 1
                        |
                      request 2
                                |
                              request 3
                                                 |
                                           request 4 - OK   
                                                      | longer-running operation 1 completes - BOOM!!!!                    

2 个答案:

答案 0 :(得分:1)

假设您有一个Web应用程序来处理具有非常常见的流的请求:

  • 预处理请求参数
  • 执行一些IO
  • 发布流程IO结果并返回客户端

IO在这种情况下可以是数据库查询,socket read \ write,文件read \ write等。

有关IO的示例,请执行文件读取和一些任意但实际的时间:

  1. 请求参数的预处理(验证等)需要1ms
  2. 文件读取(IO)需要300毫秒
  3. 后期处理需要1毫秒
  4. 现在假设有100个请求以1ms的间隔进入。使用这样的同步处理,您需要多少线程来处理这些请求?

    public IActionResult GetSomeFile(RequestParameters p) {
        string filePath = Preprocess(p);
        var data = System.IO.File.ReadAllBytes(filePath);
        return PostProcess(data);
    }
    

    嗯,显然有100个线程。由于文件读取在我们的示例中需要300毫秒,因此当第100个请求进入时 - 之前的99个被文件读取忙着阻止。

    现在让我们使用异步等待":

    public async Task<IActionResult> GetSomeFileAsync(RequestParameters p) {
        string filePath = Preprocess(p);
        byte[] data;
        using (var fs = System.IO.File.OpenRead(filePath)) {
            data = new byte[fs.Length];
            await fs.ReadAsync(data, 0, data.Length);
        }
        return PostProcess(data);
    }
    

    现在需要多少线程才能毫不拖延地处理100个请求?仍然是100.那是因为文件可以打开&#34;同步&#34;和&#34;异步&#34;模式,默认情况下,它会在&#34;同步&#34;中打开。这意味着即使您正在使用ReadAsync - 底层IO不是异步的,并且来自线程池的某些线程被阻塞等待结果。这样做我们做到了什么有用的东西吗?在网络应用的背景下 - 完全没有。

    现在让我们打开文件&#34;异步&#34;模式:

    public async Task<IActionResult> GetSomeFileReallyAsync(RequestParameters p) {
        string filePath = Preprocess(p);
        byte[] data;
        using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous)) {
            data = new byte[fs.Length];
            await fs.ReadAsync(data, 0, data.Length);
        }
    
        return PostProcess(data);
    }
    

    我们现在需要多少线程?从理论上讲,1个线程就足够了。在&#34;异步&#34;中打开文件时模式 - 读取和写入将利用(在窗口上)窗口重叠的IO。

    简单来说,它的工作原理如下:有一个类似队列的对象(IO完成端口),操作系统可以发布有关某些IO操作完成的通知。 .NET线程池注册一个这样的IO完成端口。每个.NET应用程序只有一个线程池,因此有一个IO完成端口。

    在&#34;异步&#34;中打开文件时mode - 它将其文件句柄绑定到此IO完成端口。现在当你执行ReadAsync时,在执行实际读取时 - 没有专用(对于此特定操作)线程被阻塞,等待该读取完成。当OS通知.NET完成端口时,此文件句柄的IO已完成 - .NET线程池在线程池线程上执行延续。

    现在让我们看看在我们的场景中如何处理100个间隔为1毫秒的请求:

    • 请求1进入,我们从池中获取线程以执行1ms预处理步骤。然后线程执行异步读取。它不需要阻止等待完成,因此它将返回池中。

    • 请求2进入。我们在池中已经有一个线程,它刚刚完成了请求1的预处理。我们不需要额外的线程 - 我们可以再次使用它。

    • 所有100个请求都是如此。

    • 在处理了100个请求的预处理之后,有200毫秒,直到第一个IO完成到来,我们的1个线程可以做更多有用的工作。

    • IO完成事件开始到来 - 但我们的后处理步骤也很短(1ms)。只有一个线程可以再次处理它们。

    这当然是一种理想化的场景,但它显示了&#34; async await&#34;但特别是异步IO可以帮助您保存线程&#34;。

    如果我们的后处理步骤不短但我们决定在其中执行繁重的CPU绑定工作怎么办?那么,这将导致线程池饥饿。线程池将毫无延迟地创建新线程,直到它达到可配置的低水印&#34; (您可以通过ThreadPool.GetMinThreads()获取并通过ThreadPool.SetMinThreads()进行更改)。达到该数量的线程后 - 线程池将尝试等待其中一个忙线程变为空闲。它不会永远等待,通常它会等待0.5-1秒,如果没有线程变为空闲 - 它将创建一个新的。尽管如此,在繁重的负载情况下,这种延迟可能会使您的Web应用程序变慢。因此,不要违反线程池假设 - 不要在线程池线程上运行长时间的CPU工作。

答案 1 :(得分:-1)

事实是,async / await&#34;保存线程&#34;是真理和废话的混合物。确实,它通常不仅仅是为了服务某个特定任务而创建更多的线程,但它很高兴地掩盖了这样一个事实,即在封面下有许多线程在等待完成端口上的事件,这些线程是由运行时。完成端口线程的数量大约是系统中处理器核心的数量。因此,在具有八个处理器内核的系统上,大约有八个线程在等待IO完成事件。在一个与异步IO相关的应用程序中,这很棒,但在一个没有做过多IO的应用程序中,他们大多只是坐在那里吃资源,而不是&#34;保存线程&#34;通过任何想象力。

当异步IO操作完成时,其中一个线程将被唤醒&#34;并最终在任何相关的任务上调用延续。如果所有完成线程都忙于执行continuation(可能是因为开发人员在继续操作时犯了很多CPU密集型工作的错误),当另一个IO操作完成时,在完成其中一个完成线程之前不会处理该完成起来并且能够处理它。这就是所谓的&#34;线程饥饿&#34;,这就是为什么建议在大量使用异步IO的应用程序中启动比处理器核心数更多的线程。

.NET和异步IO的问题以及async IO&#34;保存线程的一揽子理念是,很多开发人员都不了解幕后实际发生的事情,以及错误地使用async / await模式可能会使完成线程池匮乏,这很容易。

无论如何,&#34;无线&#34;这不是一个在任何意义上都没有意义的术语。

相关问题