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

Yur*_*ger 5 c# optimization multithreading asynchronous asp.net-web-api

我有一个关于 Webapi2 的问题

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

webapi2 控制器的示例:

 private async Task<Foo> Barfoo(Bar foo)
 {
     //some async function
 }     

 public async Task<IHttpActionResult> Foo(Bar bar)
 {
     List<Task> tasks=new List<Task>();
     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);
 }
Run Code Online (Sandbox Code Playgroud)

客户端只需要一个函数,所以我想要的更像是这样:

 private async Task<Foo> Barfoo(Bar foo)
 {
   //some async function  
 }     

 public async Task<IHttpActionResult> Foo(Bar bar)
 {
     List<Task> tasks=new List<Task>();
     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);
 }
Run Code Online (Sandbox Code Playgroud)

Stu*_*tLC 6

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

// Random async method here ...
private async Task<int> DelayAsync(int seconds)
{
    await Task.Delay(seconds*1000);
    Trace.WriteLine($"Done waiting {seconds} seconds");
    return seconds;
}

[HttpGet]
public async Task<IHttpActionResult> ParallelBackgroundTasks()
{
    var firstResult = await DelayAsync(6);

    // 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);
}
Run Code Online (Sandbox Code Playgroud)

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

#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
Run Code Online (Sandbox Code Playgroud)

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

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

仍然不需要等待后台工作的调用,即:

#pragma warning disable 4014
ScheduleBackGroundWork();
#pragma warning restore 4014
Run Code Online (Sandbox Code Playgroud)

笔记

  • 假设在最里面的 await 之前没有完成 CPU 绑定的工作,这种方法比 using 的优势Task.Run()在于它使用更少的线程池线程。

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

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

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

  • 使用未等待任务时,您还需要在不等待的情况下调用异步代码时放入全局未观察到的任务异常处理程序。更多关于这里

  • 如果你使用依赖注入,如果在一个未等待的 Task 之后要执行的继续有任何依赖,尤其是那些每个请求注入的依赖IDisposable,你需要欺骗你的容器以说服它不要在请求完成(因为您的延续将需要在未来运行一段时间)

编辑 - 重新可扩展性

老实说,这在很大程度上取决于您打算对“后台”任务做什么。考虑这个更新的“后台任务”:

private async Task<int> 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;
}
Run Code Online (Sandbox Code Playgroud)
  • 如果您确实需要在触及最内层await(上面的案例 1)之前执行 CPU 密集型工作 ,您将需要诉诸 Jonathan 的Task.Run()策略,将访问控制器的等待客户端与“案例 1”工作分离(否则客户端将是被迫等待)。这样做会为每个任务消耗约 1 个线程。
  • 类似地,在案例 2 中,如果您在 之后执行 CPU 密集型工作await,则计划的延续将被安排在剩余工作期间消耗线程。虽然这不会影响原始客户端调用时长,但会影响整体进程线程和 CPU 使用率。
  • 但是,如果您的后台任务除了将工作卸载到某些外部 IO 绑定活动(例如数据库、外部 Web 服务等)之外,几乎没有做任何事前和事后 IO 处理,那么剩余的任务将很快完成线程使用量可以忽略不计。
  • 对于后台等待 IO 绑定操作的持续时间,根本不应该有线程消耗(请参阅最终的There is no Thread

所以我想答案是“视情况而定”。在自托管的 Owin 服务上,您可能可以在没有预处理 + 后处理的情况下完成一些未等待的任务,但是如果您使用的是 Azure,那么像Azure 函数或较旧的Azure Web 作业之类的东西听起来像是后台的更好赌注加工。


Ste*_*ary 5

您正在寻找的是“即发即忘” ——这在 ASP.NET 上本质上是危险的

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