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

我有一个关于Webapi2的问题

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

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);
 }

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

 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);
 }
最佳答案
假设您正在尝试并行化由控制器操作调用的许多异步任务,并假设您只想在一个(明确的)任务完成后将响应返回给客户端,而不等待所有响应,(点火和忘记您可以简单地调用异步方法而无需等待它们:

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

[HttpGet]
public async Task<IHttpActionResult> 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

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

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()更有优势,因为它使用更少的线程池线程.
>即便如此,需要考虑这样做的智慧 – 尽管任务是在控制器的线程池线程上连续创建的,当IO绑定工作完成时,continuation(Trace.WriteLine)将每个都需要一个线程来完成,这可以如果所有延续同时完成,仍会导致饥饿 – 出于可伸缩性的原因,您不希望多个客户端调用这些类型的函数.
>显然,客户端实际上并不知道所有任务的最终结果是什么,因此您可能需要添加额外状态以在实际工作完成后通知客户端(例如,通过SignalR).此外,如果应用程序池死亡或被回收,结果将丢失.
>当您不等待异步方法的结果时,您还会收到编译器警告 – 这可以通过编译指示来抑制.
>使用未单点任务时,您还需要在不等待的情况下调用异步代码时放入全局未观察任务异常处理程序.更多关于这here
>如果使用依赖注入,如果在未经过任务处理的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;
}

>如果你确实需要在进入最里面之前进行CPU密集型工作
等待(上面的案例1),你需要求助于
Jonathan的Task.Run()策略来解耦等待的客户端
从“案例1”工作访问控制器(否则客户端将被迫等待).这样做会为每个任务咀嚼~1个线程.
>同样,在案例2中,如果您在等待之后进行CPU密集型工作,
然后计划的延续将被安排消耗a
线程剩余工作的持续时间.虽然这不会影响原始客户端调用持续时间,但它会影响整个进程线程和CPU使用率.
>但是,如果你的后台任务除了之外没什么作用
将工作卸载到某些外部IO绑定活动(例如数据库,外部Web服务等),很少或没有前后IO处理,然后任务的剩余将快速完成,并且线程使用可以忽略不计.
>在等待IO绑定操作的后台持续时间内,根本不应该有线程消耗(参见最终的There is no Thread)

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

转载注明原文:c# – Webapi2 – 在一个任务完成后从控制器操作返回,但继续进一步的异步处理 - 代码日志