我了解无线程异步有更多线程可用于服务输入(例如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!!!!

最佳答案

假设您有一个Web应用程序,该应用程序以非常普通的流程处理请求:

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

  • 在这种情况下,IO可以是数据库查询,套接字读\写,文件读\写等。

    以IO为例,让我们看一下文件读取以及一些任意但现实的时间安排:
  • 请求参数(验证等)的预处理需要1毫秒
  • 文件读取(IO)需要300毫秒
  • 后处理需要1毫秒的时间

  • 现在假设以1ms的间隔传入100个请求。这样的同步处理需要多少个线程来立即处理这些请求?
    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。这是因为可以在“synchornous”和“asynchronous”模式下打开文件,并且默认情况下以“synchronous”打开。这意味着,即使您使用的是ReadAsync,底层IO也不是异步的,并且线程池中的某些线程被阻塞,等待结果。通过这样做,我们取得了任何有用的成果吗?在网络应用中-完全没有。

    现在让我们以“异步”模式打开文件:
    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个线程就足够了。当您以“异步”模式打开文件时-读写将利用(在Windows上)重叠的IO窗口。

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

    以“异步”模式打开文件时-将其文件句柄绑定(bind)到此IO完成端口。现在,当您执行ReadAsync时,在执行实际读取时-没有专用线程(针对此特定操作)被阻塞,等待该读取完成。当OS通知.NET完成端口该文件句柄的IO已完成时-.NET线程池在线程池线程上执行继续。

    现在,让我们看一下在我们的场景中如何处理100个间隔为1ms的请求:
  • 请求1进入后,我们从池中获取线程以执行1ms预处理步骤。然后线程执行异步读取。它不需要阻止等待完成,因此它返回到池中。
  • 请求2进入。池中已经有一个线程,它刚刚完成了对请求1的预处理。我们不需要其他线程-我们可以再次使用该线程。
  • 所有100个请求都相同。
  • 在处理了100个请求的预处理之后,还有200毫秒,直到第一个IO完成到达,我们的1个线程可以在其中完成更多有用的工作。
  • IO完成事件开始到来-但是我们的后处理步骤也很短(1ms)。只有一个线程可以再次处理所有这些线程。

  • 当然,这是一种理想的方案,但是它显示了不是“异步等待”而是特别是异步IO如何帮助您“保存线程”。

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

    关于c# - async-await "save threads"如何?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/49837818/

    10-13 07:47
    查看更多