Webapi2 – 在一个任务完成后从控制器操作返回,但继续进一步的异步处理

我有一个关于Webapi2的问题

我的应用程序是完全async/await ,但我想优化最后一部分。 我很难找到,所以有什么办法可以做到以下几点吗?

webapi2控制器的一个示例:

  private async Task Barfoo(Bar foo) { //some async function } public async Task Foo(Bar bar) { List tasks=new List(); var actualresult=Barfoo(bar.Bar); tasks.Add(actualresult); foreach(var foobar in bar.Foo) { //some stuff which fills tasks } await Task.WhenAll(tasks); return Ok(actualresult.Result); } 

客户端只需要一个function,所以我想要的更像是:

  private async Task Barfoo(Bar foo) { //some async function } public async Task Foo(Bar bar) { List tasks=new List(); var actualresult=Barfoo(bar.Bar); return Ok(actualresult.Result); foreach(var foobar in bar.Foo) { //some stuff which fills tasks for extra logic, not important for the client } await Task.WhenAll(tasks); } 

假设您正在尝试并行化由控制器操作调用的许多异步任务,并假设您只想在一个(明确的)任务完成后将响应返回给客户端,而不等待所有响应,(点火和忘记你可以简单地调用异步方法而不必等待它们:

 // Random async method here ... private async Task DelayAsync(int seconds) { await Task.Delay(seconds*1000) .ConfigureAwait(false); Trace.WriteLine($"Done waiting {seconds} seconds"); return seconds; } [HttpGet] public async Task ParallelBackgroundTasks() { var firstResult = await DelayAsync(6) .ConfigureAwait(false); // Initiate unawaited background tasks ... #pragma warning disable 4014 // Calls will return immediately DelayAsync(100); DelayAsync(111); // ... #pragma warning enable 4014 // Return first result to client without waiting for the background task to complete return Ok(firstResult); } 

如果在完成所有后台任务后需要进行进一步处理,即使原始请求线程已完成,仍可以在完成后安排继续:

 #pragma warning disable 4014 var backgroundTasks = Enumerable.Range(1, 5) .Select(DelayAsync); // Not awaited Task.WhenAll(backgroundTasks) .ContinueWith(t => { if (t.IsFaulted) { // Exception handler here } Trace.WriteLine($"Done waiting for a total of {t.Result.Sum()} seconds"); }); #pragma warning restore 4014 

更好的方法是将后台工作重构为自己的异步方法,其中可以使用exception处理的好处:

 private async Task ScheduleBackGroundWork() { try { // Initiate unawaited background tasks var backgroundTasks = Enumerable.Range(1, 5) .Select(DelayAsync); var allCompleteTask = await Task.WhenAll(backgroundTasks) .ConfigureAwait(false); Trace.WriteLine($"Done waiting for a total of {allCompleteTask.Sum()} seconds"); } catch (Exception) { Trace.WriteLine("Oops"); } } 

后台工作的调用仍然是未加入的,即:

 #pragma warning disable 4014 ScheduleBackGroundWork(); #pragma warning restore 4014 

笔记

  • 假设在最内层等待之前没有完成CPU限制的工作,这种方法比使用Task.Run()更有优势,因为它使用更少的线程Task.Run()线程。

  • 即便如此,需要考虑这样做的智慧 – 尽管任务是在控制器的线程池线程上连续创建的,当IO绑定工作完成时,continuation( Trace.WriteLine )将每个都需要一个线程来完成,这仍然可以如果所有延续同时完成,则导致饥饿 – 出于可伸缩性的原因,您不希望多个客户端调用这些类型的函数。

  • 显然,客户端实际上并不知道所有任务的最终结果是什么,因此您可能需要添加额外状态以在实际工作完成后通知客户端(例如,通过SignalR)。 此外,如果应用程序池死亡或被回收,结果将丢失。

  • 当您不等待异步方法的结果时,您还会收到编译器警告 – 这可以通过编译指示来抑制。

  • 使用未单击的任务时,您还可能希望在不等待的情况下调用异步代码时放入全局的未观察任务exception处理程序。 更多关于这里

编辑 – 可扩展性

说实话,它很大程度上取决于你打算用’背景’任务做什么。 考虑这个更新的“后台任务”:

 private async Task DelayAsync(int seconds) { // Case 1 : If there's a lot of CPU bound work BEFORE the innermost await: Thread.Sleep(1000); await Task.Delay(seconds*1000) .ConfigureAwait(false); // Case 2 : There's long duration CPU bound work in the continuation task Thread.Sleep(1000); Trace.WriteLine($"Done waiting {seconds} seconds"); return seconds; } 
  • 如果你确实需要Task.Run()最内层await之前进行CPU密集型工作(上面的情况1),你需要采用Jonathan的Task.Run()策略来将访问Controller的等待客户端与“案例1”工作分离(否则客户将被迫等待)。 这样做会为每个任务咀嚼~1个线程。
  • 类似地,在情况2中,如果您在await之后执行CPU密集型工作,则计划的继续计划将在剩余工作期间使用一个线程。 虽然这不会影响原始客户端调用持续时间,但它会影响整个进程线程和CPU使用率。
  • 但是,如果您的后台任务除了将工作卸载到某些外部IO绑定活动(例如,数据库,外部Web服务等)以外,很少或没有前后IO处理,那么任务的剩余部分将很快完成,线程使用率可以忽略不计。
  • 对于等待IO绑定操作的后台持续时间,应该没有线程消耗(参见确定的没有线程 )

所以我猜答案是“它取决于”。 您可以在自托管的Owin服务上执行一些没有事先处理和后期处理的未完成任务,但如果您使用的是Azure,那么像Azure Web Jobs这样的事情听起来更适合后台处理。

return语句之后的代码将不会在第二个示例中执行,但可以将这些任务卸载到ThreadPool

 public async Task Foo(Bar bar) { List tasks = new List(); var actualresult = Barfoo(bar.Bar); foreach(var foobar in bar.Foo) { //some stuff which fills tasks for extra logic, not important for the client Task.Run(() => /* foobar task creation, queued on worker threads */); } // this will execute without waiting for the foobar logic to finish return Ok(actualresult.Result); } 

如果您想稍后检查“额外逻辑”任务是否完成或错误,您可能需要查看任务并行库

您正在寻找的是“一劳永逸” – 这在ASP.NET上本质上是危险的 。

正确的解决方案是使用可靠的队列(Azure队列/ MSMQ)连接到WebAPI的独立工作进程(Azurefunction/ Win32服务)。 您的WebAPI应该写入队列,然后返回响应。 工作进程(在ASP.NET之外)应该从队列中读取并处理工作项。