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

59

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

[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 { });
    }
}

我接着对同步端点进行了负载测试: 图片描述

... 然后是异步端点: 图片描述

如果我将延迟增加到1000毫秒,以下是测试结果: 图片描述 图片描述

正如您所看到的,每秒请求数并没有太大差别 - 我本来期望异步端点能处理更多请求。我错过了什么吗?


4
异步并不是一种可以让你的电脑更加努力工作的神奇药水。实际上,由于额外的开销,它甚至会使某些任务变得更慢。最终,异步只是一种更高效地利用资源的工具。 - Robbert Draaisma
13
这与现实生活几乎相同:完成一项工作需要10分钟,它会花费相应的时间。不同之处在于您等待完成的方式。是一直盯着过程(同步)还是在完成时得到通知(异步)。在两种情况下,您都可以在10分钟内交付结果,但在异步情况下,您可以在等待完成时喝杯咖啡。 - Sir Rufo
24
当你正在做晚餐并需要煮开水时,你可以坐在那里盯着水煮沸,或者你可以离开一会儿去帮助孩子完成家庭作业,然后当水烧开时再回来。如果你记录下这两种情况下水烧开所用的时间,你认为差异会有多大?(在这个比喻中,去帮助孩子是“异步”的版本,盯着水则是“同步”的版本。) - Servy
11
请按照以下设置进行操作:将线程池大小限制为10,接着在100秒内连续发送100个请求。每个请求需要在控制器中延迟10秒。然后,不要测量“每秒请求数”,而是计算所有请求的平均处理时间。异步模型应该会给出更小的值,反映其设计目标。如果您测量的内容与实际结果不直接相关,则无法观察到您想要的结果。 - Lex Li
3
谢谢您,@Lex。您的建议解决了我的问题。我的问题在于我没有限制线程池的大小,因此对于我进行的测试来说并没有真正的区别。一旦我限制了线程池的大小,就能明显看到差异。 - Carl Rippon
显示剩余9条评论
2个回答

145
是的,你忽略了async并不与速度有关,只与请求每秒的概念略微相关的事实。
Async只做一件事情。如果正在等待任务,并且该任务不涉及CPU绑定工作,并且因此线程变为空闲状态,则该线程可能被释放以返回到池中执行其他工作。
就是这样,Async简而言之。Async的目的是更有效地利用资源。在您可能拥有线程被卡住,只是坐在那里等待某些I / O操作完成的情况下,它们可以被分配到其他工作。这导致您应内部化的两个非常重要的想法:
1. Async!= 更快。事实上,Async更慢。异步操作涉及开销:上下文切换,在堆上进行数据的移动等。这会增加额外的处理时间。即使在某些情况下仅谈论微秒,Async始终比等效的同步过程慢。周期。全站点。
2. 仅在服务器负载时Async才会为您节省资源。只有当服务器处于压力状态时,Async才会给它一些急需的喘息时间,而同步可能会让它跪下。这一切都关乎规模。如果您的服务器只处理微不足道的请求数量,您很可能永远不会看到与同步相比的差异,而且正如我所说,您可能会由于涉及的开销而使用更多资源,具有讽刺意味。
这并不意味着您不应该使用Async。即使您的应用程序今天不流行,也不意味着它以后不会流行,那时重新调整所有代码以支持Async将是一场噩梦。 Async的性能成本通常可以忽略不计,如果您最终需要它,它将是救星。
更新
关于保持Async的性能成本可以忽略不计,有一些有用的提示,在大多数关于C#中的Async的讨论中不太明显或没有很好地解释。
尽可能经常使用ConfigureAwait(false)。
await DoSomethingAsync().ConfigureAwait(false);
几乎每个异步方法调用都应该跟随这个操作,但除了少数特例。 ConfigureAwait(false) 告诉运行时,在异步操作期间不需要保留同步上下文。默认情况下,当您等待异步操作时,会创建一个对象来在线程切换之间保留线程本地数据。这占用了处理异步操作所需的大部分处理时间,并且在许多情况下完全没有必要。它真正重要的地方只是像 action 方法,UI 线程等地方——那些与线程关联的信息需要被保留的地方。您只需要保留此上下文一次,因此只要例如您的 action 方法使用保持同步上下文完整性的异步操作等待,那么该操作本身可以执行其他未保留同步上下文的异步操作。因此,在像 action 方法这样的地方将对await的使用限制到最少,并尝试将多个异步操作分组到一个可以调用该 action 方法的单个异步方法中。这将减少使用异步时的开销。值得注意的是,这仅适用于 ASP.NET MVC 中的操作。ASP.NET Core 使用依赖注入模型而不是静态模型,因此不需要担心线程本地变量。在其他情况下,您可以在 ASP.NET Core 操作中使用ConfigureAwait(false),但在 ASP.NET MVC 中则不能。事实上,如果尝试在 ASP.NET MVC 中使用,则会导致运行时错误。

  • 尽可能减少需要保留的本地变量数量。在调用 await 之前初始化的变量会添加到堆中,并在任务完成后弹出。您声明的越多,就会有更多放置在堆上的内容。特别是,大型对象图表可以对此造成破坏,因为这是要移动的大量信息。有时这是不可避免的,但应该注意这一点。

  • 尽可能省略async/await关键字。例如,请考虑以下内容:

    public async Task DoSomethingAsync()
    {
        await DoSomethingElseAsync();
    }
    

    在这里,DoSomethingElseAsync返回一个被等待并解除包装的Task。然后,创建了一个新的Task来从DoSometingAsync返回。但是,如果您将方法编写为:

    public Task DoSomethingAsync()
    {
        return DoSomethingElseAsync();
    }
    

    DoSomethingAsync 直接返回由 DoSomethingElseAsync 返回的 Task。这样可以减少大量开销。


  • 3
    在ASP.NET Core中,虽然可以使用ConfigureAwait(false),但是这篇文章提到它并不是必需的:https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html - Bart Verkoeijen
    4
    尽可能减少需要保留的本地变量数量。我认为这是一种过早优化的情况。就像在函数式编程中,最小化在lambda中重复使用的本地变量数量仅因为它们需要复制到闭包实例一样。我认为,首要关注应该是代码的清晰度/可读性。在大多数情况下,分配堆变量所需的几个纳秒/微秒都是无关紧要的,特别是与控制器操作的整体上下文中发生的其他"瓶颈"相比。 - Marc Sigrist
    2
    特别是大型对象图表在这里可能会造成严重破坏,因为那是需要在堆栈上移动的大量信息。我认为这不是问题。根变量下的对象图有多大并不重要。复制到异步状态机的唯一内容是指向根对象的指针,只有 64 位。图表本身的内存保持原样,不会被复制到其他地方。或者你是指其他什么? - Marc Sigrist
    2
    不,你所描述的是异步工作的方式。在同步中,线程被保持,但它会变为非活动状态。同步和异步与CPU正在执行的任务无关。这一切都与线程有关。始终存在一组线程,每个线程消耗一定量的内存,无论它是否正在被使用。因此,可用线程的总数有一个硬性限制。在像Web服务器这样的东西中,该限制通常为1000。因此,如果所有1000个线程都在同步等待,则所有进一步的请求都将排队等待,直到其中一个线程释放为止。 - Chris Pratt
    1
    所有异步操作只是允许线程在不活跃时返回到池中,从而提供额外的空间。 - Chris Pratt
    显示剩余5条评论

    9
    请记住,async 更多的是关于扩展而不是性能。你不会看到应用程序在性能测试中的表现改进对其扩展能力产生多大的影响。要正确地测试扩展性,您需要在一个与生产环境理想情况下匹配的适当环境中进行负载测试。
    您正在尝试基于仅异步性能来进行微基准性能提升。这当然是可能的(取决于代码/应用程序),您可能会看到性能上的明显降低,这是因为异步代码中存在一些开销(上下文切换、状态机等)。话虽如此,99%的时间,您需要编写可扩展的代码(再次取决于您的应用程序)-而不是担心在这里或那里花费额外的毫秒数。在这种情况下,您没有看到整体情况,而只是注重细节。当测试async可以为您做什么时,您应该真正关注负载测试而不是微基准测试。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接