为啥我们需要异步任务以任何方式默认 Web API 请求将仅通过创建或重用现有线程来运行

Posted

技术标签:

【中文标题】为啥我们需要异步任务以任何方式默认 Web API 请求将仅通过创建或重用现有线程来运行【英文标题】:Why we need Async task any way default web API request will be running by creating or reusing existing thread only为什么我们需要异步任务以任何方式默认 Web API 请求将仅通过创建或重用现有线程来运行 【发布时间】:2019-11-25 12:18:25 【问题描述】:

为什么我们在 WebApi c# 中需要异步任务,任何方式默认的 Web API 请求都将仅通过创建或重用现有线程来运行。所以线程已经被使用了?

【问题讨论】:

如果两个人想同时连接到你的 API 怎么办?还是10?他们是否应该等待其他人的请求完成后再为他们的请求提供服务? 很抱歉,我无法回答您的问题。请考虑重写。 【参考方案1】:

不要将异步与并行混淆。

异步意味着当前线程在您等待对某些 I/O 操作的响应时被释放。 (本地存储、网络请求等)

并行是指同时运行两组或多组代码。这是多线程。

异步代码与多线程无关。实际上恰恰相反:异步代码的部分好处是不需要需要更多线程。

例如,考虑一个从数据库读取数据的 Web API 调用。当 1000 个请求同时进入时会发生什么?

如果代码是同步编写的,则每个请求都需要一个单独的线程。但是 ASP.NET 有一个最大线程数。这样就会达到最大值,其余的请求将不得不等到第一个请求完成后才能开始

如果代码是异步编写的,那么一旦发出数据库请求,线程就会在等待来自数据库的响应时被释放。在这段等待时间内,ASP.NET 可以使用该线程开始处理新请求。

结果是您需要更少的线程来完成相同数量的工作。这也意味着您可以使用相同数量的资源完成更多工作。

Microsoft 在这方面有一系列写得很好的文章值得一读:Asynchronous programming with async and await。那篇文章有一个关于做早餐的类比,有助于解释异步编程的真正含义。

【讨论】:

【参考方案2】:

谢谢,加布里埃尔,你是对的

找到更明确的答案 来自链接:https://***.com/a/49850842/958539

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

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 完成端口),操作系统可以在其中发布有关某些 IO 操作完成的通知。 .NET 线程池注册了一个这样的 IO 完成端口。每个 .NET 应用程序只有一个线程池,因此有一个 IO 完成端口。

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

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

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

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

所有 100 个请求都是如此。

在处理完 100 个请求的预处理后,距离第一个 IO 完成还有 200 毫秒,在这期间我们的 1 个线程可以做更多有用的工作。

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

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

如果我们的后处理步骤不短,而是决定在其中进行繁重的 CPU 密集型工作怎么办?那么,这将导致线程池饥饿。线程池将立即创建新线程,直到达到可配置的“低水位线”(您可以通过 ThreadPool.GetMinThreads() 获取并通过 ThreadPool.SetMinThreads() 更改)。达到该线程数量后 - 线程池将尝试等待其中一个繁忙的线程空闲。它当然不会永远等待,通常它会等待 0.5-1 秒,如果没有线程空闲 - 它会创建一个新线程。尽管如此,在重负载情况下,这种延迟可能会大大降低您的 Web 应用程序的速度。所以不要违反线程池假设——不要在线程池线程上运行长时间的 CPU 密集型工作。

【讨论】:

以上是关于为啥我们需要异步任务以任何方式默认 Web API 请求将仅通过创建或重用现有线程来运行的主要内容,如果未能解决你的问题,请参考以下文章

如何知道以编程方式在我当前的应用程序中运行了多少异步任务/服务?

Django配置Celery执行异步和同步任务(tasks))

如何从客户端取消异步任务

CompletableFuture,可变长度的Runnable任务以异步方式运行

如何在现代JavaScript中编写异步任务

为啥我们需要中间件用于 Redux 中的异步流?