当前位置: 首页 > 知识库问答 >
问题:

异步等待如何“保存线程”?

龙新荣
2023-03-14

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

假设我们只有3个线程

Thread 1 | 
Thread 2 |
Thread 3 |

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

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 Wait,您可以

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

共有2个答案

柴霖
2023-03-14

事实上,异步/等待“保存线程”的概念是事实和胡说八道的混合。的确,它通常并不只是为了服务于特定的任务而创建更多的线程,但它很高兴地掩盖了这样一个事实,即在幕后有许多线程在等待由运行时创建的完成端口上的事件。完成端口线程的数量大约是系统中处理器内核的数量。因此,在具有八个处理器核的系统上,大约有八个线程等待IO完成事件。在一个疯狂使用异步IO的应用程序中,这很好,但在一个没有这么多IO的应用程序中,他们大多只是坐在那里吃资源,而不是想尽一切办法“节省线程”。

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

的问题。NET和异步IO以及异步IO“保存线程”这一笼统的概念是,许多开发人员不了解幕后实际发生的事情,而错误地使用异步/等待模式会使完成线程池耗尽,这太容易了。

在任何情况下,“无Thread”一词在这里都没有任何意义。

吕俊哲
2023-03-14

假设您有一个web应用程序,它使用非常常见的流处理请求:

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

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

对于IO的示例,让我们以文件读取和一些任意但现实的计时为例:

  1. 请求参数的预处理(验证等)需要1ms

现在假设有100个请求以1ms的间隔出现。您需要多少线程来处理这些请求而不会延迟像这样的同步处理?

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。这是因为文件可以在“synchornous”和“asynchronous”模式下打开,默认情况下,它以“synchronous”模式打开。这意味着,即使您使用的是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);
}

我们现在需要多少线程?现在理论上,一条线就足够了。当您以“异步”模式打开文件时,读写操作将利用(在windows上)windows重叠IO。

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

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

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

>

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

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

    所有100个请求都是如此。

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

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

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

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

  •  类似资料:
    • 我试图在react/electron项目中使用async/await,但它不起作用。我想要的是获取docker容器状态列表。但是安慰。日志(列表)返回未定义的。 有人能帮我吗?:)

    • 我最近阅读了有关async/await的文章,我感到困惑的是,我阅读的许多文章/帖子都指出,在使用async await(示例)时不会创建新线程。 我创建了一个简单的控制台应用程序来测试它 以下代码的输出是: 我想知道,如果没有创建其他线程,部分在哪里运行?如果它运行在同一个线程上,难道它不应该因为长的I/O请求而阻塞它吗?或者编译器足够聪明,如果它需要太长的时间,就可以将该操作移到另一个线程上,

    • 我正在尝试将数据库调用移出控制器,以清理并使其可测试。当它们在控制器中时,一切都会顺利进行。我将它们移出控制器,并添加了一个异步,以确保我们等待。否则,我将调用的中的函数。现在,一旦我使用async/await,控制器中的函数就会认为没有用户,因为它没有等待。 有几个关于异步等待的SO问题,但我没有找到一个解决我的问题。我确实验证了返回了我的用户,并添加了控制台日志来显示路径。 节点猫鼬异步等待似

    • 我正在编写一个WinForms应用程序,它将数据传输到USB HID类设备。我的应用程序使用了优秀的通用HID库V6.0,可以在这里找到。简单来说,当我需要向设备写入数据时,这是被调用的代码: 当我的代码退出while循环时,我需要从设备中读取一些数据。但是,设备无法立即响应,因此我需要等待此呼叫返回后再继续。由于当前存在,RequestToGetInputReport()声明如下: GetInp

    • 我需要在函数中运行各种步骤,但步骤必须按特定顺序运行。我尝试实现一个函数,如下所示: 控制台中的预期结果应该是1,2,3,但我得到了3,2,1。似乎忽略了参数。 编辑功能在上面的示例中仅用于模拟繁重的任务。在我的项目中,我不会使用它。事实上,我需要连接到一个数据库,然后在进入下一步之前重新格式化结果。即使包括,2也会在1之前登录。换句话说,空列表被传递到我的图形,因为没有考虑。以下是我当前的代码:

    • 我通读了Dart/flatter中的Async/Await/then,试图理解为什么aysnc函数中的Await不会等到完成后再继续。在我的UI中,有一个按钮调用一个异步方法来返回一个位置,该位置总是返回null,并且不等待函数完成。 该函数将调用推送到一个新的UI页面,该页面选择一个位置,并应返回一个结果。如何使该函数等待结果?我不是在使用异步吗?