异步等待如何“保存线程"? [英] How does async-await "save threads"?

查看:83
本文介绍了异步等待如何“保存线程"?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我了解到无线程异步有更多线程可用于服务输入(例如HTTP请求),但是我不明白当异步操作完成并且需要一个线程时,这怎么可能不会导致线程饥饿运行它们的延续.

I understand that with threadless async there are more threads available to service inputs (e.g. a HTTP request), but I don't understand how that doesn't potentially cause cause thread starvation when the async operations complete and a thread is needed to run their continuation.

假设我们只有3个线程

Thread 1 | 
Thread 2 |
Thread 3 |

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

and they get blocked on long-running operations that require threads (e.g. make database query on separate db server)

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!!!!

通过异步等待,您可以像这样

With async-await you can make this like

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   

但是,在我看来,这似乎可能会导致运行中"的异步操作过多,并且如果太多同步操作同时结束,则没有线程可用于继续执行它们.

However, this seems to me like it could result in a surplus of async operations that are "in-flight" and if too many finish at the same time then there are no threads available to run their continuation.

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应用程序,该应用程序以非常常见的流程处理请求:

Suppose you have web application which handles a request with a very common flow:

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

IO可以是数据库查询,套接字读取\写入,文件读取\写入等.

IO in this case can be database query, socket read\write, file read\write and so on.

以IO为例,我们来进行文件读取和一些任意但现实的计时:

For an example of IO let's take file reading and some arbitrary but realistic timings:

  1. 预处理请求参数(验证等)需要1毫秒
  2. 文件读取(IO)需要300毫秒
  3. 后处理需要1毫秒

现在假设100个请求以1ms的间隔进入.像这样的同步处理,您需要多少个线程来立即处理这些请求?

Now suppose 100 requests come in with interval of 1ms. How many threads you will need to handle those requests without delay with synchronous processing like this?

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

好吧,显然有100个线程.由于在我们的示例中文件读取需要300毫秒,因此当发出第100个请求时-前99个文件正忙于文件读取.

Well, 100 threads obviously. Since file read takes 300ms in our example, when 100th request comes in - previous 99 are busy blocked by file reading.

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

Now let's "use async await":

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也不是异步的,并且线程池中的某些线程被阻塞,等待结果.通过这样做,我们取得了任何有用的成果吗?在网络应用中-完全没有.

How many threads are needed now to handle 100 requests without delay? Still 100. That's because file can be opened in "synchornous" and "asynchronous" modes, and by default it opens in "synchronous". That means even though you are using ReadAsync - underlying IO is not asynchronous and some thread from a thread pool is blocked waiting for result. Did we achieve anything useful by doing that? In context of web applicaiton - not at all.

现在让我们以异步"模式打开文件:

Now let's open file in "asynchronous" mode:

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窗口.

How many threads we need now? Now 1 thread is enough, in theory. When you open file in "asynchronous" mode - reads and writes will utilize (on windows) windows overlapped IO.

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

In simplified terms it works like this: there is a queue-like object (IO completion port) where OS can post notifications about completions of certain IO operations. .NET thread pool registers one such IO completion port. There is only one thread pool per .NET application, so there is one IO completion port.

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

When file is opened in "asynchronous" mode - it binds its file handle to this IO completion port. Now when you do ReadAsync, while actual read is performed - no dedicated (for this specific operation) thread is blocked waiting for that read to complete. When OS notify .NET completion port that IO for this file handle has completed - .NET thread pool executes continuation on thread pool thread.

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

Now let's see how processing of 100 requests with 1ms interval can go in our scenario:

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

  • Request 1 goes in, we grab thread from a pool to execute 1ms pre-processing step. Then thread performs asynchronous read. It doesn't need to block waiting for completion, so it returns to the pool.

请求2进入.我们池中已经有一个线程,它刚刚完成了对请求1的预处理.我们不需要其他线程-我们可以再次使用该线程.

Request 2 goes in. We have a thread in a pool already which just completed pre-processing of request 1. We don't need an additional thread - we can use that one again.

对于所有100个请求都相同.

Same is true for all 100 requests.

在处理了100个请求的预处理之后,还有200毫秒,直到第一个IO完成到达为止,我们的1个线程可以在其中完成更多有用的工作.

After handling pre-processing of 100 requests, there are 200ms until first IO completion will arrive, in which our 1 thread can do even more useful work.

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

IO completion events start to arrive - but our post-processing step is also very short (1ms). Only one thread again can handle them all.

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

This is an idealized scenario of course, but it shows how not "async await" but specifically asynchronous IO can help you to "save threads".

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

What if our post-processing step is not short but we instead decided to do heavy CPU bound work in it? Well, that will cause thread pool starvation. Thread pool will create new threads without delay, until it reaches configurable "low watermark" (which you can obtain via ThreadPool.GetMinThreads() and change via ThreadPool.SetMinThreads()). After that amount of threads is reached - thread pool will try to wait for one of the busy threads to become free. It will not wait forever of course, usually it will wait for 0.5-1 seconds and if no thread become free - it will create a new one. Still, that delay might slow your web application quite a bit in heavy load scenarios. So don't violate thread pool assumptions - don't run long CPU-bound work on thread pool threads.

这篇关于异步等待如何“保存线程"?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆