【问题标题】:Why should I return Task<IActionResult> in a Controller?为什么我应该在 Controller 中返回 Task<IActionResult>?
【发布时间】:2021-03-26 11:47:21
【问题描述】:

所以我已经尝试掌握了相当长的一段时间,但看不出将每个控制器端点声明为异步方法的意义。

让我们看一个 GET-Request 来可视化问题。

这是我处理简单请求的方式,只需完成工作并发送响应即可。

[HttpGet]
public IActionResult GetUsers()
{
      // Do some work and get a list of all user-objects.
      List<User> userList = _dbService.ReadAllUsers(); 
      return Ok(userList);
} 

下面是我经常看到的async Task&lt;IActionResult&gt; 选项,它与上面的方法相同,但方法本身返回一个Task。有人可能会认为,这个更好,因为您可以有多个请求进入并且它们异步处理但我测试了这种方法和上面的方法,结果相同。两者都可以同时接受多个请求。那么我为什么要选择这个签名而不是上面的那个呢?我只能看到这样的负面影响,例如由于异步将代码转换为状态机。

[HttpGet]
public async Task<IActionResult> GetUsers()
{
      // Do some work and get a list of all user-objects.
      List<User> userList = _dbService.ReadAllUsers(); 
      return Ok(userList);
} 

下面的这种方法也是我无法理解的。我看到很多代码都具有这种设置。一种async 方法await 然后返回结果。像这样等待会使代码再次顺序化,而不是获得多任务/多线程的好处。我错了吗?

[HttpGet]
public async Task<IActionResult> GetUsers()
{
      // Do some work and get a list of all user-objects.
      List<User> userList = await _dbService.ReadAllUsersAsync(); 
      return Ok(userList);
} 

如果你能用事实启发我,那就太好了,这样我就可以像现在一样继续发展,或者知道由于误解了这个概念而我做错了。

【问题讨论】:

  • 因为异步操作返回一个任务,在任务完成之前它的结果是不可用的。
  • 其实你应该这样看:当你创建一个基于任务的方法时,你本质上是在说你的方法使用了一些异步操作。它与调用者本身无关。调用者只知道它应该等待操作。

标签: c# asp.net .net .net-core


【解决方案1】:

请阅读"Synchronous vs. Asynchronous Request Handling" section of Intro to Async/Await on ASP.NET

两者都可以同时接受多个请求。

是的。这是因为 ASP.NET 是多线程的。因此,在同步情况下,您只需有多个线程调用相同的操作方法(在不同的控制器实例上)。

对于非多线程平台(例如 Node.js),您必须使代码异步处理同一进程中的多个请求。但在 ASP.NET 上它是可选的。

这样等待会使代码再次顺序化,而不是享受多任务/多线程的好处。

是的,它是顺序的,但它不是同步的。它是顺序,因为async 方法一次执行一个语句,并且该请求在async 方法完成之前不会完成。但它不是同步的——同步代码也是顺序的,但它阻塞一个线程直到方法完成。

那我为什么要选择这个签名而不是上面那个呢?

如果您的后端可以扩展,那么异步操作方法的好处就是可扩展性。具体来说,异步操作方法会在异步操作进行时产生它们的线程 - 在这种情况下,GetUsers 在数据库执行查询时不会占用线程。

在测试环境中很难看到好处,因为您的服务器有空闲线程,所以调用异步方法 10 次(占用 0 个线程)和调用同步方法 10 次(占用最多 10 个线程,另外还有 54 个线程)。你可以artificially restrict the number of threads in your ASP.NET server and then do some tests看看有什么不同。

在现实世界的服务器中,您通常希望使其尽可能异步,以便您的线程可用于处理其他请求。或者,如here所述:

请记住,异步代码不会取代线程池。这不是线程池异步代码;它是线程池异步代码。异步代码允许您的应用程序充分利用线程池。它使用现有的线程池并将其增加到 11。

记住上面的“如果”;这尤其适用于现有代码。如果您只有一个 SQL 服务器后端,并且几乎所有操作都查询数据库,那么将它们更改为异步may not be useful,因为可伸缩性瓶颈通常是数据库服务器而不是 Web 服务器。但是,如果您的 Web 应用程序可以使用线程来处理非数据库请求,或者如果您的数据库后端是可扩展的(NoSQL、SQL Azure 等),那么将操作方法​​更改为异步可能会有所帮助。

对于新代码,我默认推荐异步方法。异步可以更好地利用服务器资源并且对云更友好(即,按需付费托管成本更低)。

【讨论】:

  • @ZesaRex:NoReadAllUsersAsync 将查询发送到数据库然后返回一个未完成的任务。当该任务为awaited 时,线程返回线程池并可以在数据库执行查询时处理其他请求。
  • @ZesaRex:Thread.Sleep 的异步等效项是 await Task.Delay,它不会阻塞线程。如果您谈论的是使用async 而不使用await 的实际代码,则会出现编译器警告,通知您该方法实际上将同步运行。
  • 好,这意味着我的问题是“是”! :) 谢谢
【解决方案2】:

如果您的数据库服务类有一个async 方法来获取用户,那么您应该会看到好处。一旦请求发送到数据库,它就会等待另一端服务返回的网络或磁盘响应。在执行此操作时,可以释放线程来执行其他工作,例如服务其他请求。在非异步版本中,线程只会阻塞并等待响应。

使用async 控制器操作,您还可以获得CancellationToken,如果由于另一端的客户端已终止连接而发出令牌信号,您可以提前退出(但这可能不适用于所有 Web 服务器)。

[HttpGet]
public async Task<IActionResult> GetUsers(CancellationToken ct)
{
    ct.ThrowIfCancellationRequested();

    // Do some work and get a list of all user-objects.
    List<User> userList = await _dbService.ReadAllUsersAsync(ct); 
    return Ok(userList);
} 

因此,如果您有一组昂贵的操作并且客户端断开连接,您可以停止处理请求,因为客户端永远不会收到它。这可以释放应用程序的时间和资源来处理客户端对返回结果感兴趣的请求。

但是,如果您有一个不需要async 调用的非常简单的操作,那么我可能不会担心它,并保持原样。

【讨论】:

  • 使用CancellationToken 来取消操作非常好。我认为,如果您进一步扩展该概念以及为什么它对您的回答有帮助,这将很有帮助,因为它将作为当前主题的重要信息。
【解决方案3】:

你在这里遗漏了一些基本的东西。

当您使用async Task&lt;&gt; 时,您实际上是在说“异步运行所有 I/O 代码并释放处理线程以...处理”。

在规模上,您的应用每秒将能够处理更多请求,因为您的 I/O 不会占用 CPU 密集型工作,反之亦然。

您现在没有看到太多好处,因为您可能正在本地进行测试,手头有大量资源。

【讨论】:

  • 您的第一个示例绑定了一个线程。您的第二个示例无法编译,因为没有 await。只要没有后端瓶颈,第三个示例就可以扩展。而且,是的,我已经测试过这种场景。
  • 谢谢。顺便说一下,第二个示例实际上可以编译。它只会被视为同步。
  • 多哈。你说得对。我习惯于在 IDE 中看到红色波浪下划线并添加 async。我将警告设置为错误。
【解决方案4】:

结果从一个异步(非阻塞)操作返回一个任务,它代表一些应该完成的工作。任务可以告诉您工作是否已完成,如果操作返回结果,则任务会为您提供在任务完成之前不可用的结果。您可以从官方 Microsoft Docs 了解有关 C# 异步编程的更多信息:

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-5.0

【讨论】:

    【解决方案5】:

    如您所知,ASP.NET 基于请求执行的多线程模型。简单地说,每个请求都在自己的线程中运行,这与其他单线程执行方式如 nodejs 中的事件循环相反。

    现在,线程是一种有限资源。如果您耗尽了线程池(可用线程),您将无法继续有效地处理任务(或者在大多数情况下根本无法)。如果您对操作处理程序使用同步执行,则您会占用池中的这些线程(即使它们不需要)。了解这些线程处理请求很重要,它们不进行输入/输出。 I/O 由独立于 ASP.NET 请求线程的独立进程处理。因此,如果在您的操作中,您有一个数据库提取操作需要 15 秒才能执行,那么您将强制当前线程等待 idle 15 秒以返回数据库结果,以便它可以继续执行代码。因此,如果您有 50 个这样的请求,那么您将在基本睡眠模式下占用 50 个线程。显然,这不是非常可扩展的,并且很快就会成为一个问题。为了有效地使用您的资源,最好在达到 I/O 操作时保留正在执行的请求的状态,同时释放线程以处理另一个请求。当 I/O 操作完成时,状态被重新组合并提供给池中的空闲线程以恢复处理。这就是您使用异步处理的原因。它可以让您更有效地使用有限资源并防止此类线程饥饿。当然,这不是最终的解决方案。它可以帮助您扩展应用程序以获得更高的负载。如果您不需要它,请不要使用它,因为它只会增加开销。

    【讨论】:

      【解决方案6】:

      异步操作返回一个任务(它不是结果,而是一个承诺,一旦任务完成就会有结果。

      异步方法应该是awaited,以便等待任务完成。如果您 await 使用异步方法,则返回值将不再是任务,而是您预期的结果。

      想象一下这个异步方法:

      async Task<SomeResult> SomeMethod()
      {
          ...
      } 
      

      以下代码:

      var result = SomeMethod(); // the result is a Task<SomeResult> which may have not completed yet
      
      var result = await SomeMethod(); // the result is type of SomeResult
      

      现在,我们为什么要使用async 方法而不是同步方法:

      想象一个方法正在做一些可能需要很长时间才能完成的工作,当它完成同步时,所有其他请求将等待这个耗时工作的执行完成,因为它们都发生在同一个线。但是,当它是异步时,任务将在一个(可能是另一个)线程中完成,并且不会阻塞其他请求。

      【讨论】:

      • 因为异步操作不会阻塞您的其他代码并在另一个线程中运行。让我更新我的答案。
      猜你喜欢
      • 2017-11-06
      • 1970-01-01
      • 2019-07-30
      • 2019-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-05
      • 2019-09-18
      相关资源
      最近更新 更多