【问题标题】:If we have an ASP.NET async action that returns a task's result, should you omit the async/await keywords?如果我们有一个返回任务结果的 ASP.NET 异步操作,是否应该省略 async/await 关键字?
【发布时间】:2020-02-29 14:16:24
【问题描述】:

如果我有以下代码:

public async Task<SomeObject> ActionMethodAsync()
{
   return await someAsyncMethod();
}

我应该把它改成:

public SomeObject ActionMethod()
{
   return someAsyncMethod().GetAwaiter().GetResult();
}

我不确定一旦控制权返回给ActionMethodAsync 的调用者,究竟会发生什么以及是否实现了任何有益的结果。

会不会产生和following编译器编译的代码类似的结果,或者是IIS给的一个线程池线程,如果我们走await路线就不会被阻塞?我觉得我们仍然会使用单个线程,无论我们选择继续使用哪个选项。 (无需切换线程上下文和异步状态机的额外成本。)

【问题讨论】:

  • 第一个不阻塞,第二个阻塞。它们是完全不同的,如果你能提供帮助,你不想同步等待 async 方法。您可以在不改变语义的情况下将其更改为return someAsyncMethod()(没有async,但保持返回类型相同)。
  • @JeroenMostert 不会将未完成的任务返回给执行该操作的用户吗?
  • @SpiritBob:当然——这就是重点。 ASP.NET 框架是最终等待您的任务(或者更确切地说是调度延续,效率更高)的框架,而不是某些最终用户。
  • 当然,虽然它实际上是为了在您的控制器中允许完整的async / await。直接返回另一个async 方法的结果的情况有点不寻常。为了一致性起见,大多数人仍然会将方法写成async,然后再写return await,以防你以后需要使事情变得更复杂,即使这与直接返回结果完成相同的事情并且它有一点点更多开销(因为编译器仍然在后台生成一个完整的异步状态机)。
  • ASP.NET Core 特别是完全异步的——最新版本实际上是contain code that will produce errors,当它们检测到同步 I/O 时,因为已发现这会导致线程池饥饿。这就是他们多么希望你一路走async。 ASP.NET“仅仅”添加了async 支持,但即使那是7 年前的事了(查看文章上的日期)。让所有请求代码异步运行(将其与线程解耦)对可扩展性有很大好处。

标签: c# asp.net multithreading asynchronous async-await


【解决方案1】:

您链接到的那个答案是在谈论一个控制台应用程序,所以这是一个不同的故事。

微软有一些写得很好的关于Asynchronous programming with async and await 的文章值得一读。它用于制作早餐的示例非常有助于理解异步编程的全部内容。

要记住的关键是 ASP.NET 可以使用的线程数是有上限的。 It's configurable,但总会有一个最大值。所以一个线程能做的越多越好。

让我们以一些实数为例。每个 CPU 的默认线程数为 20。假设您使用的是四核处理器。最大线程数将是 80(如果将超线程计算为额外的 CPU,它实际上可能是 160,但我们会说 80)。

现在假设有 80 个 HTTP 请求同时进入,所有这些都需要发出 I/O 请求(例如,对数据库)。它们都被同时处理并开始它们的 I/O 请求。现在,当他们等待响应时,另一个请求进来了。它会发生什么取决于您的代码是否是异步的。

  • 如果您的代码是同步的,则所有 80 个线程都被锁定,等待响应。因此,在这 80 个请求中的一个完成之前,第 81 个请求不会得到处理。
  • 如果您的代码是异步的,所有 80 个线程都将空闲,并且将立即处理第 81 个请求。当每个 I/O 请求完成时,它会放在这 80 个线程中的一个上完成并返回一个响应给调用者。

异步编程并不是要更快地返回单个请求(事实上,它会慢一点)。这与您的应用程序的整体性能有关。

现在,转到您的示例。您没有考虑一个选项,那就是直接返回 Task 而不等待它:

public Task<SomeObject> ActionMethodAsync()
{
   return someAsyncMethod();
}

这将略微(可能实际上并不明显)更快。 an old question about that 有一些很好的答案,但总之:

  • 如果你只调用一个异步方法,并且你将返回它的结果,那么直接返回Task
  • 如果您需要调用多个异步方法,或者需要对异步调用的结果进行一些处理,则必须使用asyncawait

【讨论】:

  • 我猜如果您将返回的任务包装在 try/catch 块中,您需要等待它,因为不会捕获异常 - 您将直接返回任务, 正确的? ExceptionFilters 应该工作。
  • 正确,完全正确。而您是否要这样做取决于您要处理异常的位置。在那里处理它可能是有意义的,或者只是让调用方法处理它。
【解决方案2】:

使用async/await 将允许 IIS 线程暂时停止等待任务的请求(通常是异步 i/o 请求)、执行其他工作(例如处理新传入的请求、恢复之前等待任务的其他请求)并继续当相应的任务完成时,它们可以提高吞吐量。

来源: https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth#what-does-this-mean-for-a-server-scenario

【讨论】:

    【解决方案3】:

    还有更多。

    以下是您应该坚持使用async/await 的一些原因

    1。你不应该使用GetResult

    TaskAwaiter.GetResult Method 状态

    此 API 支持产品基础架构,并非旨在直接从您的代码中使用。

    2。没有await,您的代码不再是异步的

    通过删除await,您将摆脱异步魔法,您的代码变得同步和阻塞,并且每个请求都使用一个线程,直到完成。


    工作量很大,线程不多。

    我希望这个小例子能够展示一个框架在使用Tasks 时处理更多请求是多么容易,然后有线程。

    可以将其想象为 3 名服务员可以为 10 张桌子提供服务。

    static async Task DoAsync(int index) 
    {
        var threadId = Thread.CurrentThread.ManagedThreadId;
        await Task.Delay(TimeSpan.FromMilliseconds(index*100));
        Console.WriteLine($"task: {index}, threads: {threadId} -> {Thread.CurrentThread.ManagedThreadId}");
    }
    
    public static async Task Main()
    {
        Console.WriteLine($"thread: {Thread.CurrentThread.ManagedThreadId}: Main");
    
        var tasks = Enumerable.Range(0, 10).Select( i => DoAsync(i));
        await Task.WhenAll(tasks);
    
        Console.WriteLine($"thread: {Thread.CurrentThread.ManagedThreadId}: Main Done");
    }
    

    输出

    thread: 1: Main
    task: 0, threads: 1 -> 1
    task: 1, threads: 1 -> 4
    task: 2, threads: 1 -> 4
    task: 3, threads: 1 -> 4
    task: 4, threads: 1 -> 4
    task: 5, threads: 1 -> 4
    task: 6, threads: 1 -> 5
    task: 7, threads: 1 -> 5
    task: 8, threads: 1 -> 5
    task: 9, threads: 1 -> 5
    thread: 5: Main Done
    

    正如您发现的那样,n 线程能够处理 x&gt;n 请求的意义在 Async in depth“这对服务器场景意味着什么?”中明确表达了?部分。

    此模型适用于典型的服务器场景工作负载。因为没有专门用于阻塞未完成任务的线程,所以服务器线程池可以为更多的 Web 请求提供服务。 (...)

    状态机

    Asynchronous programming 是一个很好的关于 async/await 的信息来源。有两段与await == GetAwaiter().GetResult() 的简化视图特别相关。

    幕后发生了什么

    有很多涉及异步操作的移动部分。如果您对 Task 和 Task 背后发生的事情感到好奇,请查看Async in-depth 文章了解更多信息。

    在 C# 方面,编译器将您的代码转换为一个状态机,该状态机跟踪诸如在达到等待时产生执行以及在后台作业完成时恢复执行等事情。

    【讨论】:

    • 只是在你的代码中有等待,不会让它异步。如果 Async 方法中没有挂起,或者这些挂起能够同步完成(没有任务开始/工作计划),你会同步完成,只会阻碍自己所有添加的“魔法”。
    • 我仍然不明白在这种情况下使用 await 对我们有什么帮助?无论如何都将使用单个线程。当调用方法挂起并将控制权交还给ActionMethodAsync 时,ActionMethodAsyncawait 将把控制权交还给该方法的调用者(无论是什么)。那个线程不在那里,然后就等着吗?
    • 如果同时有多个请求怎么办?
    • @SpiritBob 使用 async/await 将允许 IIS 线程暂时停止等待任务的请求,做其他工作(服务新传入的请求,恢复之前等待任务的其他请求),并在相应的任务已完成,从而允许更高的吞吐量。
    • @SpiritBob:谢谢我在这里找到它,docs.microsoft.com/en-us/dotnet/standard/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-31
    相关资源
    最近更新 更多