c# - 如何 async-await "save threads"?

标签 c# .net multithreading asynchronous async-await

我知道使用无线程异步有更多线程可用于服务输入(例如 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可以是数据库查询、socket读写、文件读写等。

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

  • 现在假设 100 个请求以 1 毫秒的间隔进入。像这样的同步处理需要多少线程来无延迟地处理这些请求?
    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。那是因为文件可以在“同步”和“异步”模式下打开,默认情况下它以“同步”打开。这意味着即使您使用的是 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 完成端口),操作系统可以在其中发布有关某些 IO 操作完成的通知。 .NET 线程池注册了一个这样的 IO 完成端口。每个 .NET 应用程序只有一个线程池,因此有一个 IO 完成端口。

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

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

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

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

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

    相关文章:

    c# - 在 UTF16 列中存储 UTF8 数据

    java - 在 for 循环声明中初始化的变量的范围实际上不仅仅是 block 范围吗?

    Java 线程检查非 volatile 变量的变化似乎要花很长时间

    c# - 快速 C# 线程类

    android - Executors.newFixedThreadPool(size) 是要考虑 CPU 内核,还是我应该考虑。值得吗?

    c# - 从 TXT 文件中读取所有行并仅在每行 C# 的每个第一个字符串中搜索

    c# - 如何将自定义信息存储到 Outlook AppointmentItem

    .net - 无法在自定义端口上打开 dockerized .net core api

    .net - .NET 和/或 ASP.NET 项目是否有推荐的、建议的或常规的结构?

    c# - 使用 DataTable 时出现 IndexOutOfRange 异常