ASP.NET核心同步和异步控制器操作之间没有太大区别

Car*_*pon 27 c# asp.net-mvc threadpool async-await asp.net-core

我在控制器中编写了几个操作方法来测试ASP.NET内核中同步异步控制器操作之间的区别:

[Route("api/syncvasync")]
public class SyncVAsyncController : Controller
{
    [HttpGet("sync")]
    public IActionResult SyncGet()
    {
        Task.Delay(200).Wait();

        return Ok(new { });
    }

    [HttpGet("async")]
    public async Task<IActionResult> AsyncGet()
    {
        await Task.Delay(200);

        return Ok(new { });
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我加载测试同步终点: 在此输入图像描述

...后跟异步终点: 在此输入图像描述

如果我将延迟增加到1000毫秒,这是结果 在此输入图像描述 在此输入图像描述

正如您所看到的,每秒请求数没有太大差异- 我希望异步终点每秒处理更多请求.我错过了什么吗?

Chr*_*att 65

是的,您错过了异步与速度无关的事实,并且与每秒请求的概念仅略微相关.

异步只做一件事,只做一件事.如果一个任务正在等待,而且任务不涉及CPU绑定工作,其结果是,在线程变为空闲,然后,该线程可能会被释放返回到池做其他的工作.

而已.简而言之,异步.异步的关键是更有效地利用资源.在你可能有线程被捆绑的情况下,只是坐在那里敲击他们的脚趾,等待一些I/O操作完成,他们可以改为负责其他工作.这导致您应该内化的两个非常重要的想法:

  1. 异步!=更快.事实上,异步更.异步操作涉及开销:上下文切换,数据在堆上和堆栈之间进行混洗等.这增加了额外的处理时间.即使在某些情况下我们只谈微秒,异步也总是比同等的同步过程慢.期.完全停止.

  2. 如果您的服务器处于负载状态,Async只会购买任何东西.只有在您的服务器受到压力时,async会给它一些急需的呼吸空间,而同步可能会让它瘫痪.一切都与规模有关.如果你的服务器只能处理少量的请求,你很可能永远不会看到同步的差异,就像我说的那样,你可能最终会使用更多资源,具有讽刺意味的是,因为涉及到开销.

这并不意味着你不应该使用异步.即使您的应用程序今天不受欢迎,但这并不意味着它不会更晚,并且在此时重新调整所有代码以支持异步将是一场噩梦.异步的性能成本通常可以忽略不计,如果你最终需要它,它将是一个节省生命的人.

UPDATE

在保持异步的性能成本可以忽略不计的情况下,有一些有用的提示,在大多数关于C#中的异步讨论中都不是很明显或者说得很清楚.

  • ConfigureAwait(false)尽可能多地使用.

    await DoSomethingAsync().ConfigureAwait(false);
    
    Run Code Online (Sandbox Code Playgroud)

    除了一些特定的异常之外,几乎所有的异步方法调用都应该遵循.ConfigureAwait(false)告诉运行时您不需要在异步操作期间保留同步上下文.默认情况下,等待异步操作时,会创建一个对象以保留线程切换之间的线程本地.这占用了处理异步操作所涉及的大部分处理时间,并且在许多情况下完全没有必要.唯一真正重要的地方是诸如动作方法,UI线程等等 - 这些地方的信息与需要保留的线程相关联.您只需要保留一次此上下文,因此只要您的操作方法(例如,等待同步上下文完整的异步操作),该操作本身就可以执行其他异步操作,而不保留同步上下文.因此,您应该将await操作方法等内容的使用限制在最低限度,而是尝试将多个异步操作组合到该操作方法可以调用的单个异步方法中.这将减少使用异步所涉及的开销.值得注意的是,这只是ASP.NET MVC中的操作问题.ASP.NET Core使用依赖注入模型而不是静态,因此没有线程本地需要关注.在别人对你可以使用ConfigureAwait(false)在ASP.NET核心作用,但不是在ASP.NET MVC.实际上,如果您尝试,您将收到运行时错误.

  • 您应尽可能减少需要保留的本地数量.在调用await之前初始化的变量将添加到堆中,并在任务完成后弹出后退.你声明的越多,进入堆的次数就越多.特别是大型对象图可以在这里造成严重破坏,因为大量的信息可以在堆上移动.有时这是不可避免的,但这是值得注意的.

  • 如果可能,请删除async/ awaitkeywords.请考虑以下示例:

    public async Task DoSomethingAsync()
    {
        await DoSomethingElseAsync();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    在这里,DoSomethingElseAsync返回一个Task等待和解包的东西.然后,Task创建一个new 来返回DoSometingAsync.但是,如果相反,您将方法编写为:

    public Task DoSomethingAsync()
    {
        return DoSomethingElseAsync();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    Task通过返回DoSomethingElseAsync被直接返回DoSomethingAsync.这减少了大量的开销.

  • “您应该尽可能减少需要保留的局部变量的数量。”:我认为这是一种过早优化的情况。这就像在函数式编程中,最大限度地减少 lambda 中重用的局部变量的数量,只是因为它们需要复制到闭包实例中。在我看来,首先关心的应该是代码的清晰度/可读性。在大多数情况下,分配堆变量所需的几纳/微秒是无关紧要的,特别是与控制器操作的整体上下文中发生的其他“瓶颈”相比。 (4认同)
  • 虽然您可以在 ASP.NET Core 中使用“ConfigureAwait(false)”,但这篇文章提到不需要它:https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html (3认同)
  • “特别是大型对象图可能会造成严重破坏,因为需要在堆上移动和移出大量信息。”:我认为情况并非如此。根变量下的对象图有多大并不重要。唯一复制到异步状态机的是指向根对象的指针,它只是 64 位。图的内存本身保留在原来的位置,不会复制到任何地方。或者你还有别的意思吗? (2认同)
  • 不,您所描述的是异步的工作原理。在同步中,线程被保留,但它会变为非活动状态。同步或异步与 CPU 正在执行的操作无关。这都是关于线程的。总是有一个线程池,每个线程都会消耗一定量的内存,无论它是否正在被积极使用。因此,可用线程总数存在硬性限制。在 Web 服务器之类的系统中,该限制通常为 1000。因此,如果所有 1000 个线程都同步等待,则所有进一步的请求都会排队,直到这 1000 个线程之一释放为止。 (2认同)

Dav*_*ack 7

请记住,这async更多是关于扩展而不是性能。根据上面的性能测试,您不会看到应用程序扩展能力的改进。要正确测试扩展,您需要在与您的生产环境理想匹配的适当环境中进行负载测试。

您正在尝试仅基于 async 对性能改进进行微基准测试。您可能会看到性能明显下降(取决于代码/应用程序)。这是因为异步代码(上下文切换、状态机等)存在一些开销。也就是说,在 99% 的情况下,您需要编写代码以进行扩展(同样,取决于您的应用程序)——而不是担心在这里或那里花费的任何额外毫秒。在这种情况下,可以这么说,您不是只见树木不见森林。在测试async可以为您做什么时,您应该真正关注负载测试而不是微基准测试