在 web api 控制器(.net 核心)中使用 async/await 或任务

Posted

技术标签:

【中文标题】在 web api 控制器(.net 核心)中使用 async/await 或任务【英文标题】:Using async/await or task in web api controller (.net core) 【发布时间】:2017-06-16 15:00:28 【问题描述】:

我有一个 .net 核心 API,它有一个控制器,用于构建一个聚合对象以返回。 它创建的对象由来自对服务类的 3 个方法调用的数据组成。这些都是相互独立的,可以相互隔离运行。 目前我正在使用任务来提高这个控制器的性能。当前版本看起来像这样......

[HttpGet]
public IActionResult myControllerAction()
      
    var data1 = new sometype1();
    var data2 = new sometype2();
    var data3 = new List<sometype3>();

    var t1 = new Task(() =>  data1 = service.getdata1(); );
    t1.Start();

    var t2 = new Task(() =>  data2 = service.getdata2(); );
    t2.Start();

    var t3 = new Task(() =>  data3 = service.getdata2(); );
    t3.Start();

    Task.WaitAll(t1, t2, t3);

    var data = new returnObject
    
         d1 = data1,
         d2 = data2,
         d2 = data3
    ;

    return Ok(data);

这很好,但是我想知道使用任务是否是最好的解决方案?使用 async/await 会是更好的主意和更被接受的方式吗?

例如,控制器是否应该被标记为异步并在每次调用服务方法时放置等待?

【问题讨论】:

service 不为getdata 方法提供异步替代方案吗?此外,通常您应该更喜欢Task.Run(),而不是创建任务然后手动启动它们。 async/await 在这个意义上对并行性没有帮助。实际上,它会序列化您的 3 个服务请求。但是,当您执行 Task.WaitAll 时,它会有所帮助,因为您正在用该行停放一个线程。 根本没有理由创建冷任务然后对它们调用Start。它们不是线程。只需使用Task.Run。然后使用await Task.WhenAll 【参考方案1】:

据我了解,您希望它并行执行,所以我认为您的代码没有任何问题。正如 Gabriel 所说,您可以等待任务完成。

[HttpGet]
public async Task<IActionResult> myControllerAction()
      
  var data1 = new sometype1();
  var data2 = new sometype2();
  var data3 = new List<sometype3>();

  var t1 = Task.Run(() =>  data1 = service.getdata1(); );
  var t2 = Task.Run(() =>  data2 = service.getdata2(); );
  var t3 = Task.Run(() =>  data3 = service.getdata3(); );

  await Task.WhenAll(t1, t2, t3); // otherwise a thread will be blocked here

  var data = new returnObject
  
      d1 = data1,
      d2 = data2,
      d2 = data3
  ;

 return Ok(data);

您还可以使用任务的结果来保存一些代码行并使代码整体“更好”(参见 cmets):

[HttpGet]
public async Task<IActionResult> myControllerAction()
      
  var t1 = Task.Run(() => service.getdata1() );
  var t2 = Task.Run(() => service.getdata2() );
  var t3 = Task.Run(() => service.getdata3() );

  await Task.WhenAll(t1, t2, t3); // otherwise a thread will be blocked here

  var data = new returnObject
  
      d1 = t1.Result,
      d2 = t2.Result,
      d2 = t3.Result
  ;

 return Ok(data);

【讨论】:

第一个 sn-p 包含几个问题 - 用一次性对象初始化变量,然后捕获这些变量。 我同意这不是最优的,这就是为什么我用我的第二个代码 sn-p 提供了一个“更好”的解决方案。无论如何,我不认为第一次剪断有真正的问题。编译器会将一次性局部变量转换为私有字段,这应该可以正常工作。 (再一次,我知道这并不理想。) 至少你应该使用 Task 作为方法类型,因为异步使用 @CanPERK:你说得对,我已经添加了建议。【参考方案2】:

这很好用,但是我想知道使用任务是否是最好的解决方案?使用 async/await 会是更好的主意和更被接受的方式吗?

是的,当然。在 ASP.NET 上进行并行处理会消耗每个请求的多个线程,这会严重影响您的可伸缩性。对于 I/O,异步处理要好得多。

要使用async,首先从您的最低级别调用开始,在您的服务中的某处。它可能会在某个时候进行 HTTP 调用;将其更改为使用异步 HTTP 调用(例如,HttpClient)。然后让async从那里自然生长。

最终,您将得到异步的 getdata1Asyncgetdata2Asyncgetdata3Async 方法,它们可以同时使用:

[HttpGet]
public async Task<IActionResult> myControllerAction()

  var t1 = service.getdata1Async();
  var t2 = service.getdata2Async();
  var t3 = service.getdata3Async();
  await Task.WhenAll(t1, t2, t3);

  var data = new returnObject
  
    d1 = await t1,
    d2 = await t2,
    d3 = await t3
  ;

  return Ok(data);

使用这种方法,当三个服务调用正在进行时,myControllerAction 使用 个线程而不是 四个

【讨论】:

对returnObject中的每个任务都做await Task.WhenAll()await不是多余的吗?不知道为什么仅仅做一个或另一个是不够的。你能解释一下吗? :) 如果返回类型不同,你必须await每个任务。即使在这种情况下,我也更喜欢Task.WhenAll,因为它使代码更清晰。它也更有效率(只在上下文中恢复一次而不是 3 次),但我的主要原因是代码清晰。 @StephenCleary 如果我的 API 需要使用 WaitForExit 调用 System.Diagnostics.Process,如何使用零线程?我计划实施***.com/a/10789196/241004 作为解决方法。你有更好的主意吗? @JawadAlShaikh:不,这是你能做的最好的事情。 Process 只是没有对异步非常友好的 API。 :// @StephenPatten:是的。【参考方案3】:
[HttpGet]
public async Task<IActionResult> GetAsync()
      
    var t1 = Task.Run(() => service.getdata1());
    var t2 = Task.Run(() => service.getdata2());
    var t3 = Task.Run(() => service.getdata3());

    await Task.WhenAll(t1, t2, t3);

    var data = new returnObject
    
        d1 = t1.Status == TaskStatus.RanToCompletion ? t1.Result : null,
        d2 = t2.Status == TaskStatus.RanToCompletion ? t2.Result : null,
        d3 = t3.Status == TaskStatus.RanToCompletion ? t3.Result : null
    ;

   return Ok(data);

    当您等待任务时,您的操作线程当前被阻塞。使用 TaskWhenAll 返回可等待的任务对象。因此,使用异步方法,您可以等待任务而不是阻塞线程。 您可以使用Task&lt;T&gt; 返回所需类型的结果,而不是创建局部变量并在任务中分配它们。 不用创建和运行任务,而是使用Task&lt;TResult&gt;.Run方法 我建议对动作名称使用约定 - 如果动作接受 GET 请求,它的名称应该以 Get 开头 接下来,您应该检查任务是否成功完成。这是通过检查任务状态来完成的。在我的示例中,如果某些任务未成功完成,我使用 null 值作为返回对象属性。您可以使用另一种方法 - 例如如果某些任务失败,则返回错误。

【讨论】:

我想你需要解释一下RanToCompletion的检查。你期待提前取消吗?如果任何调用出错,await Task.WhenAll 将引发异常 您确定可以在 WebApi 中重命名 get 方法吗?在 MVC 中,你不能不改变行为。 @Sefe 是的,你是对的。没注意是mvc core,一会儿会还原一些web-api相关的改动

以上是关于在 web api 控制器(.net 核心)中使用 async/await 或任务的主要内容,如果未能解决你的问题,请参考以下文章

模型绑定不适用于 asp.net 核心 Web api 控制器操作方法中的 Stream 类型参数。(即使使用自定义流输入格式化程序)

使用 .net 核心 Web API 对 .net 控制台应用程序进行身份验证

.net 核心 Web API - 415 不支持的媒体类型

操作过滤器在 asp.net 核心 Web API 中不起作用

ASP.Net Web Api Core - 无法在抽象基类中使用 CreatedAtRoute

如何在使用 JWT 的 asp.net 核心 web 应用和 web api 中使用谷歌身份验证