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

Not much difference between ASP.NET Core sync and async controller actions

我在控制器中写了几个动作方法来测试 syncasync 控制器动作在 [=32= 中的区别]核心:

[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 毫秒后的结果

如您所见,每秒请求数 没有太大差异 - 我希望异步端点每秒处理更多请求。我错过了什么吗?

是的,您忽略了异步与速度无关的事实,它与每秒请求数的概念略有相关。

Async 做一件事而且只做一件事。如果正在等待一个任务,并且该任务不涉及 CPU 绑定工作,结果线程变为空闲,则该线程 可能 可以被释放到 return 到池中做其他工作。

就是这样。简而言之,异步。异步的要点是更有效地利用资源。在您可能已将线程捆绑在一起的情况下,只是坐在那里轻敲脚趾,等待某些 I/O 操作完成,他们可以被分配其他工作。这导致您应该内化两个非常重要的想法:

  1. 异步 != 更快。事实上,异步 更慢 。异步操作涉及开销:上下文切换、数据在堆上和堆外混洗等。这会增加额外的处理时间。即使在某些情况下我们只谈论微秒,异步也会 总是 比等效的同步过程慢。时期。句号。

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

这并不意味着您不应该使用异步。即使您的应用程序今天不流行,也不意味着以后不会流行,并且在那个时候重新调整所有代码以支持异步将是一场噩梦。异步的性能成本通常可以忽略不计,如果您最终确实需要它,它将成为您的救星。

更新

关于保持异步的性能成本可以忽略不计,有一些有用的提示,这些提示在 C# 中的大多数异步讨论中并不明显或没有很好地阐明。

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

    await DoSomethingAsync().ConfigureAwait(false);
    

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

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

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

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

    在这里,DoSomethingElseAsync return 是一个等待和解包的 Task。然后,从 DoSometingAsync 创建一个新的 Task 到 return。但是,如果相反,您将方法写为:

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

    DoSomethingElseAsync 编辑的 Task return 直接由 DoSomethingAsync 编辑 return。这减少了大量开销。

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

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