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

时间:2017-12-28 22:19:51

标签: c# asp.net-mvc asp.net-core async-await threadpool

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

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

然后加载测试同步终点: enter image description here

...后跟异步终点: enter image description here

如果我将延迟增加到1000毫秒,结果如下 enter image description here enter image description here

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

2 个答案:

答案 0 :(得分:47)

是的,您错过了async与速度无关的事实,并且与每秒请求的概念仅略有关系。

异步做一件事,只有一件事。如果正在等待任务,并且该任务不涉及CPU绑定的工作,结果,线程变为空闲,那么,该线程可能可以被释放以返回池执行其他工作。

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

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

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

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

    <强>更新

    在保持异步的性能成本可以忽略不计的情况下,有一些有用的提示,在C#中的异步讨论中,这些提示并不是很明显或非常明确。

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

      await DoSomethingAsync().ConfigureAwait(false);
      

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

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

    • 如果可能,请忽略async / await个关键字。请考虑以下示例:

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

      此处,DoSomethingElseAsync返回等待和解包的Task。然后,创建一个新的Task以从DoSometingAsync返回。但是,如果相反,您将方法编写为:

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

      Task返回的DoSomethingElseAsyncDoSomethingAsync直接返回。这减少了大量的开销。

答案 1 :(得分:1)

请记住,select Config.value('(/root/path1/path2/path3)[1]','nvarchar(max)') from [MyDB].[dbo].[MyTable] 不仅仅是性能方面的扩展。根据上面的性能测试,您不会看到应用程序扩展能力的提高。为了正确测试扩展,您需要在理想与您的产品环境相匹配的适当环境中进行负载测试。

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