c# - Webapi2-完成一项任务后从 Controller 操作返回,但继续进行进一步的异步处理

标签 c# multithreading asynchronous asp.net-web-api optimization

我有一个关于Webapi2的问题

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

一个webapi2 Controller 的示例:

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

最佳答案

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

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

如果您需要在所有后台任务完成后进行进一步处理,即使原始请求线程已完成,也可以在完成后安排继续:
#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()的优势在于它使用的线程池线程更少。
  • 即便如此,还是要考虑这样做的智慧-尽管任务是在 Controller 的线程池线程上串行创建的,但是当IO绑定(bind)工作完成时,延续(Trace.WriteLine)都将需要一个线程来完成,这仍然可能导致如果所有延续同时完成,则会出现饥饿-由于可伸缩性的原因,您不希望多个客户端调用此类功能。
  • 显然,客户端实际上并不知道所有任务的最终结果是什么,因此您可能需要添加额外的状态以在实际工作完成后(例如通过SignalR)通知客户端。另外,如果应用程序池死亡或被回收,结果将丢失。
  • 当您不等待异步方法的结果时,还会收到编译器警告-这可以通过编译指示来抑制。
  • 使用未等待的任务时,您还想在不等待等待的情况下调用异步代码时放入全局的“未观察到的任务异常”处理程序。有关此here
  • 的更多信息
  • 如果您使用依赖注入(inject),并且在未等待的任务之后要执行的继续执行具有任何依赖关系,尤其是那些按请求注入(inject)并且是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密集型工作,然后再打到最里面await(上面的案例1),您可能需要诉诸于
    乔纳森(Jonathan)的Task.Run()策略使等待的客户端脱钩
    从“案例1”工作访问Controller(否则客户端将被迫等待)。这样做将消耗每个任务约1个线程。
  • 同样,在案例2中,如果您在await之后进行CPU密集型工作,
    则预定的连续性将消耗掉
    剩余工作期间的线程。尽管这不会影响原始客户端调用的持续时间,但会影响整个进程线程和CPU使用率。
  • 但是,如果您的后台任务除了
    将工作卸载到某些外部IO绑定(bind)事件(例如数据库,外部Web服务等),而几乎没有或没有IO前后处理,那么Task的剩余部分将很快完成,线程使用率可以忽略不计。
  • 在后台等待的IO绑定(bind)操作的持续时间内,根本不应该使用任何线程(请参阅权威性There is no Thread)

  • 因此,我猜答案是“取决于情况”。在自托管的Owin服务上,您可能不需要进行一些前/后处理,而无需执行一些未处理的任务,但是,如果您使用的是Azure,则Azure functions或更旧的Azure Web Jobs之类的声音听起来可能是更好的后台处理选择。

    关于c# - Webapi2-完成一项任务后从 Controller 操作返回,但继续进行进一步的异步处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45626713/

    相关文章:

    c++ - 多线程总和

    java - 实现时间滑动窗口类时使用原子类型的非锁定线程代码

    c# - 跟踪 c#/.NET 任务流

    multithreading - 如何让node.js中的线程休眠而不影响其他线程?

    c# - 在 ASP.NET Core 中使用 wkhtmltopdf、docker 将 HTML 转换为 PDF

    c# - 在 C# 中删除文本文件的第一行

    java - Scala 在线程中对待 "sharing"局部变量的方式与 Java 不同?它是如何工作的?

    java - Spring Integration Java DSL - 异步执行多个服务激活器?

    c# - Razor 引擎模板中的类

    C# - 在控制台应用程序中精确使用 Thread.Sleep