为什么我们需要 WebApi c# 中的异步任务,默认 Web API 请求将仅通过创建或重用现有线程来运行。所以线程已经被使用了?
2 回答
不要将异步与并行混淆。
异步意味着当前线程在您等待对某些 I/O 操作的响应时被释放。(本地存储、网络请求等)
并行意味着同时运行两组或多组代码。这是多线程。
异步代码与多线程无关。实际上恰恰相反:异步代码的部分好处是不需要更多线程。
例如,考虑一个从数据库读取数据的 Web API 调用。当 1000 个请求同时进入时会发生什么?
如果代码是同步编写的,则每个请求都需要一个单独的线程。但是 ASP.NET 有一个最大线程数。这样就会达到最大值,其余的请求将不得不等到一些第一个请求完成后才能开始。
如果代码是异步编写的,那么一旦发出数据库请求,线程就会在等待来自数据库的响应时被释放。在这段等待时间内,ASP.NET 可以使用该线程开始处理新请求。
结果是您需要更少的线程来完成相同数量的工作。这也意味着您可以使用相同数量的资源完成更多工作。
微软在这方面有一系列写得很好的文章值得一读:使用 async 和 await 进行异步编程。那篇文章有一个关于做早餐的类比,它有助于解释异步编程的真正含义。
谢谢,加布里埃尔,你是对的
从链接中找到更明确的答案:https ://stackoverflow.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 线程池在线程池线程上执行延续。
现在让我们看看在我们的场景中如何处理 100 个间隔为 1 毫秒的请求:
请求 1 进入,我们从池中获取线程以执行 1ms 的预处理步骤。然后线程执行异步读取。它不需要阻塞等待完成,所以它返回到池中。
请求 2 进入。我们在池中已经有一个线程,它刚刚完成了请求 1 的预处理。我们不需要额外的线程 - 我们可以再次使用它。
所有 100 个请求也是如此。
在处理完 100 个请求的预处理后,还有 200ms 直到第一个 IO 完成到达,在这期间我们的 1 个线程可以做更多有用的工作。
IO 完成事件开始到达 - 但我们的后处理步骤也很短(1ms)。只有一个线程再次可以处理它们。
这当然是一个理想化的场景,但它显示了不是“异步等待”而是特别是异步 IO 可以帮助您“节省线程”。
如果我们的后处理步骤并不短,而是我们决定在其中进行繁重的 CPU 密集型工作怎么办?那么,这将导致线程池饥饿。线程池将立即创建新线程,直到达到可配置的“低水位线”(您可以通过 ThreadPool.GetMinThreads() 获取并通过 ThreadPool.SetMinThreads() 更改)。达到该线程数量后 - 线程池将尝试等待其中一个繁忙的线程空闲。它当然不会永远等待,通常它会等待 0.5-1 秒,如果没有线程空闲 - 它会创建一个新线程。尽管如此,在重负载情况下,这种延迟可能会大大降低您的 Web 应用程序的速度。所以不要违反线程池假设——不要在线程池线程上运行长时间的 CPU 绑定工作。