首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >async-await如何“保存线程”?

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

Stack Overflow用户
提问于 2018-04-15 09:56:59
回答 2查看 1.1K关注 0票数 5

我知道无线程异步有更多的线程可用于服务输入(例如HTTP请求),但我不明白当异步操作完成并且需要一个线程来运行它们的继续时,这如何不会潜在地导致线程饥饿。

假设我们只有3个线程

代码语言:javascript
复制
Thread 1 | 
Thread 2 |
Thread 3 |

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

代码语言:javascript
复制
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你可以让它像这样

代码语言:javascript
复制
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   

然而,在我看来,这可能会导致多余的“正在运行”的异步操作,如果太多的操作同时完成,那么就没有可用的线程来运行它们的继续。

代码语言:javascript
复制
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!!!!                    
EN

回答 2

Stack Overflow用户

发布于 2018-04-16 14:30:49

假设你有一个web应用程序,它用一个非常普通的流程处理一个请求:

client Preprocess request parameters

  • Perform IO
  • 后期处理IO结果并返回到client

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

作为IO的一个例子,让我们来看看文件读取和一些任意但现实的计时:

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

现在假设100个请求以1ms的间隔进入。像这样的同步处理,你需要多少个线程来无延迟地处理这些请求?

代码语言:javascript
复制
public IActionResult GetSomeFile(RequestParameters p) {
    string filePath = Preprocess(p);
    var data = System.IO.File.ReadAllBytes(filePath);
    return PostProcess(data);
}

好吧,显然是100个线程。由于在我们的示例中,文件读取需要300ms,因此当第100个请求进入时-前99个请求被文件读取阻塞。

现在让我们“使用async await":

代码语言:javascript
复制
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。这是因为文件可以在“同步”和“异步”模式下打开,默认情况下它以“同步”模式打开。这意味着即使您使用的是ReadAsync -底层IO也不是异步的,线程池中的一些线程会被阻塞,等待结果。我们通过这样做得到了什么有用的东西吗?在web应用程序的上下文中-根本不是。

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

代码语言:javascript
复制
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);
}

我们现在需要多少线程?从理论上讲,现在一个线程就足够了。当你在“异步”模式下打开文件时-读取和写入将利用(在windows上)窗口重叠IO。

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

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

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

  • 请求1进入,我们从一个池中抓取线程执行1ms的预处理步骤。然后线程执行异步读取。它不需要阻塞等待完成,所以它返回到pool.
  • Request 2。我们在一个池中已经有一个线程刚刚完成了对请求1的预处理。我们不需要额外的线程--我们可以再次使用这个线程。对于所有100个requests.
  • After处理100个请求的预处理,
  • 也是如此,离第一个IO完成还有200ms,在这期间,我们的1个线程可以做更有用的工作。
  • IO完成事件开始到达-但我们的后处理步骤也很短(1ms)。再一次只有一个线程可以处理所有的线程。

当然,这是一个理想化的场景,但它展示了如何不是“异步等待”,而是特别是异步IO可以帮助您“保存线程”。

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

票数 4
EN

Stack Overflow用户

发布于 2018-04-15 11:49:48

事实是,async/await“节省线程”的概念是事实和胡扯的混合体。诚然,它通常并不涉及创建更多的线程来服务于特定的任务,但它很高兴地掩盖了一个事实,即在完成端口上有许多线程在等待事件,这些事件是由运行时创建的。完成端口线程的数量大约是系统中处理器核心的数量。因此,在一个有8个处理器核心的系统上,大约有8个线程在等待IO完成事件。在一个被异步IO搞得焦头烂额的应用程序中,这是很棒的,但在一个不做太多IO的应用程序中,它们通常只是坐在那里消耗资源,而不是想象力所能想到的“节省线程”。

当异步IO操作完成时,其中一个线程将“唤醒”,并最终调用任何相关任务的延续。当另一个IO操作完成时,如果所有完成线程都在忙于执行延续(可能是因为开发人员犯了在延续中执行大量CPU密集型工作的错误),那么直到其中一个完成线程被释放并能够处理它时,该完成才会被处理。这就是所谓的“线程匮乏”,这也是为什么在大量使用异步IO的应用程序中,建议启动比处理器核心数量更多的线程的原因。

.NET和异步IO的问题,以及异步IO“节省线程”的笼统概念,是许多开发人员不了解幕后实际发生的事情,并且以可能导致完成线程池匮乏的方式滥用异步/等待模式太容易了。

在任何情况下,“无线程”在这里都不是一个有意义的术语。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/49837818

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档