我应该使用 Task.Run 还是 Task.FromResult?

Posted

技术标签:

【中文标题】我应该使用 Task.Run 还是 Task.FromResult?【英文标题】:Should I use Task.Run or Task.FromResult? 【发布时间】:2021-12-19 16:36:28 【问题描述】:

我有一个控制器,它调用一个服务方法,这个方法调用同步一个存储过程

服务:

public class ServiceSomething : IServiceSomething, IService 
   public int ProcessSomethingInDatabase(List<SqlParameter> sqlParameters)
      IConnection connection = ConnectionFactory.GetConnection();
      //This makes a synchronous call
      DataTable dataTable = Connection.DalDataTable("sp_process_something", sqlParameters);
      int result = GetResultFromDataTable(dataTable);
      return result;
     

控制器:

public class SomethingController : Controller 
   private readonly IServiceSomething _serviceSomething;

   [HttpPost]
   public async Task<CustomResult> ProcessSomethingInDatabase(Criteria criteria)
      List<SqlParameter> sqlParameters = CriteriaToSqlParams(criteria);
      int result = await Task.Run(() => _serviceSomething.ProcessSomethingInDatabase(sqlParameters));
      return new CustomResult(result);
   

该过程可能需要很长时间(有时需要 30 秒到 1 小时)。

问题在于同步调用冻结了应用程序,因此我们使用了 Task.Run。有人要求我不要在控制器中初始化线程。

现在我想知道什么是最好的实现,只要不冻结应用程序并处理可能需要数小时才能完成的过程。

我真的应该为此创建一个线程吗?

public async Task<int> ProcessSomethingInDatabaseAsync(List<SqlParameter> sqlParameters)
       IConnection connection = ConnectionFactory.GetConnection();
       return await Task.Run(() => 
          DataTable dataTable = Connection.DalDataTable("sp_process_something", sqlParameters);
          int result = GetResultFromDataTable(dataTable);
          return result;
       );
  

控制器是

[HttpPost]
public async Task<CustomResult> ProcessSomethingInDatabase(Criteria criteria)
   List<SqlParameter> sqlParameters = CriteriaToSqlParams(criteria);
   int result = await _serviceSomething.ProcessSomethingInDatabaseAsync(sqlParameters);
   return new CustomResult(result);

还是应该使用 Task.FromResult?

public Task<int> ProcessSomethingInDatabaseAsync(List<SqlParameter> sqlParameters)
       IConnection connection = ConnectionFactory.GetConnection();
       DataTable dataTable = Connection.DalDataTable("sp_process_something", sqlParameters);
       int result = GetResultFromDataTable(dataTable);
       return Task.FromResult(result);

注意: 该服务托管在 Windows 服务上,并通过 WCF 进行通信

【问题讨论】:

the synchronous call freezes the application - 然后修复该应用程序,使其不会在其网络调用中冻结。在 ASP.NET 方面,这很好。无论如何,请求在完成之前都不会返回,这方面没有任何异步。在控制器中执行 Task.Run 是 considered harmful. 两者都不:您需要DalDataTableAsync 方法。你应该await实际async方法一直到执行存储过程的方法。 @madreflection 什么是真正的异步方法? 如果这个数据库调用可能需要几分钟/几小时,那么它应该在 http 调用的范围之外完成!查看IHostedService implementations,这是在与 Web 服务相同的应用程序中实现此功能的众多方法之一(还有很多其他方法) 【参考方案1】:

您的问题的简短答案是没有一个,让我们看看为什么。

在您尝试设计解决方案的方式中至少存在几个问题。

首先,您声称您尝试实施的操作可能需要 1 小时的处理时间。这意味着您不得在 HTTP 请求的上下文中执行该操作。 HTTP 请求旨在快速,任何可能需要超过几秒时间的操作不应通过 HTTP 实现。 Web 客户端、Web 服务器和 Web 基础设施都是为快速处理 HTTP 请求而设计的,并且到处都有超时,这将不允许您在 HTTP 请求中执行操作。

您可以使用 HTTP 请求要求您的后端执行长时间运行的操作。您的 Web 堆栈将处理请求并决定您请求的任务是否可以启动(基于您的业务规则),但仅此而已:任务的实际执行必须委托给后端服务(例如通过使用队列)。

这是一个很大的话题,但我希望你明白:你不能使用动作方法来执行长时间运行的操作;您的操作方法应该只验证执行操作的请求并将实际执行委托给其他人。您可以阅读this blog post 以获取有关此方法的更多信息。

需要注意的第二点是Task.Run的用法。 使用Task.Run的唯一正当理由是从 UI 线程调用 CPU 密集型工作负载(例如,从 Windows 窗体应用程序的事件处理程序调用长时间运行的 CPU 密集型工作负载)。而已。如果您处于除此之外的其他场景中,您应该避免使用Task.Run。如果您有一个本质上是异步的操作,例如数据库查询,您应该使用异步 api 来执行该操作,并在async 方法中使用await 的结果。在现代 .NET 代码中,完全支持异步 API,因此您始终可以使用它们来执行 IO 操作。如果您使用的是不提供异步 API 的数据访问库,请更改您的数据访问库。

Task.Run 的使用在 ASP.NET 应用程序的上下文中特别危险。在 ASP.NET 应用程序中,您有一个线程池,运行时使用这些线程池来处理传入的 HTTP 请求。每次调用Task.Run 时,您都在借用其中一个线程来执行工作负载(由您传递给Task.Run 的委托表示)。您不应该这样做,因为线程是 Web 服务器中的重要资源,它们应该用于处理传入的 HTTP 请求并由 ASP.NET 运行时处理。通过为您的 Task.Run 执行借用一个线程,您正在干扰 ASP.NET 线程池管理,您不应该这样做

总结一下,如果您正在编写 ASP.NET 代码:

每次需要执行真正异步的工作负载时使用异步 API,例如数据库查询或对 Web 服务的 HTTP 请求。 await async 方法内部的异步操作的结果,并且 never 使用诸如 Task.Wait()Task.Result 之类的 API 阻止异步操作 如果您需要在操作方法中执行 CPU 绑定的同步工作负载,只需执行此操作并使用 Task.Run 包装您的方法调用,然后 do not 伪造异步。一般来说,不要在 ASP.NET 代码中使用Task.Run:唯一有意义的地方是 UI 客户端应用程序,例如 Windows 窗体或 WPF 应用程序。每次需要从 UI 线程调用长时间运行的 CPU 绑定工作负载时使用 Task.Run,这样您就不会冻结应用程序 UI。 切勿在 HTTP 请求中执行持续时间超过几秒的操作。 HTTP 请求旨在快速处理。使用队列机制将长时间运行的任务的执行委托给后端服务。

考虑阅读this blog post,了解有关Task.Run 用法的更多信息。

您在问题中提到的关于Task.FromResult&lt;T&gt; 的最后说明。此方法旨在创建一个Task 实例,该实例表示成功完成的异步操作,具有指定的结果。而已。 这不是伪造异步的方法(一般来说,您应该避免伪造异步),它只是解决为已经完成的操作创建一个Task 实例并产生特定结果的问题的一种方法。

【讨论】:

以上是关于我应该使用 Task.Run 还是 Task.FromResult?的主要内容,如果未能解决你的问题,请参考以下文章

Task.Run使用默认线程池

使用 Task.Run() 时如何避免 OutOfMemoryException? [复制]

对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

使用 Task.Run() 时如何限制最大线程数?

关于 Task.Start() 、 Task.Run() 和 Task.Factory.StartNew() 的使用

Task.Run() 与创建 Task 实例然后 Start() 一样吗?