3

繼續我有一個問題關於Webapi2Webapi2 - 從控制器動作返回一個任務完成後,但隨着進一步的異步處理

我的應用程序是完全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); 
} 

只是一個功能,所以我要的是更多這樣的:

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

我不認爲這是去工作。服務器只允許來自每個客戶端的一個連接。這樣做是爲了防止黑客在網頁上發起服務攻擊。因此,同時對多個操作執行異步操作會將服務器視爲服務攻擊。 – jdweng

+0

這不正是發生了什麼,對吧?我的意思是,一個功能是私人的,我只想在客戶端斷開後處理任務 –

+0

如果您有Resharper或配置了「StyleCop」,您將收到有關「代碼無法到達的通知」。 – Fabio

回答

2

假設你正在試圖並行的若干由控制器操作調用異步任務,並假設你只想要一個(明確的)任務完成後返回一個響應返回給客戶端,無需等待所有響應,(發射後不管)你可以簡單地調用異步方法無需等待着他們:

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

如果在完成所有後臺任務後,即使原始請求線程已完成,仍需要進一步處理,但仍然是po ssible完成後安排的延續:

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

的調用後臺工作仍然是unawaited,即:

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

注意

  • 假設有不CPU綁定完成工作之前的最內AWAIT,這種方法具有超過在於它使用較少的線程池線程使用Task.Run()的優點。儘管任務是在控制器的線程池線程上串行創建的,但是當IO界限工作完成時,延續(Trace.WriteLine)每個都需要一個線程來完成,因此,如果所有的延續都是同時完成的話,這仍然會導致飢餓 - 您不會希望多個客戶端調用這些類型的函數,這是出於可伸縮性原因。

  • 顯然,客戶端實際上並不知道所有任務的最終結果結果是什麼,所以您可能需要添加額外的狀態以在實際工作完成後通知客戶端(例如,通過SignalR)。此外,如果應用程序池死亡或被回收,結果將會丟失。

  • 您還可以得到一個編譯器警告,當你不等待異步方法的結果 - 這可以通過編譯被抑制。

  • 當使用未等待的任務時,您也可能想在不等待的情況下調用異步代碼時放入全局未觀察任務異常處理程序。更多關於此here

編輯 - 再擴展性

說實話,這將取決於很多你打算用「背景」的任務做什麼。考慮這個更新的「後臺任務」:

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,以上),你會需要求助於 喬納森的Task.Run()策略去耦等待客戶 從「外殼1的工作訪問控制器(否則客戶端將被迫等待)。這樣做會咀嚼每項任務約1個線程。
  • 類似地,在情況2中,如果在執行await, 之後執行CPU密集型工作,則在剩餘工作的持續時間內計劃繼續計劃將消耗 線程。雖然這不會影響原始客戶端調用持續時間,但會影響整個進程線程和CPU使用率。
  • 但是,如果你的後臺任務很少做其他比 卸載工作的一些外部IO勢必活動(例如數據庫,外部Web服務等),很少或根本沒有前置和後置IO處理,然後將任務的剩餘將會很快完成,線程的使用將會微不足道。
  • 對於後臺的持續時間等待IO綁定操作,應該是完全沒有線消耗量(見明確There is no Thread

所以我想答案是「看情況」。你也許可以矇混過關,沒有前+後處理上自託管Owin服務幾unawaited的任務,但如果你使用Azure的,然後像Azure Web Jobs聽起來像後臺處理一個更好的選擇。

+0

回覆:「智慧需要考慮」 - [更多這裏](https: //stackoverflow.com/a/25275835/314291) – StuartLC

+0

謝謝,我看到這個問題的可擴展性,但你會建議什麼?操作是沉重的,需要完成,問題是,感覺我不能發送一個事件來處理另一個服務,因爲參數不能被序列化。 –

+0

我更新了一些想法,但正如你在@jonathantyson的評論中提到'Azure'那樣[有更好的選擇](https://docs.microsoft.com/en-us/azure/architecture/最佳做法/後臺作業),這將爲任務完成提供更大的保證,並且不會耗盡Web服務器上的線程池。 – StuartLC

2

return語句後面的代碼不會在第二個示例中執行,但可以將這些任務卸載到ThreadPool

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

如果你想以後檢查完成或錯誤的「額外的邏輯」的任務,你可能想看看進入Task Parallel Library

+0

這看起來像解決方案,謝謝!一個小問題:它會繼續運行嗎?我知道azure webapp有一個屬性'永遠在線',這是強制性的嗎? –

+1

是的,這些任務將在'Foo'返回後繼續運行。我不確定'永遠在線',這可能是針對另一個問題。後臺任務仍然需要足夠的時間才能完成或出錯。是否20分鐘的價值聲稱[這裏](https://serverfault.com/a/620791)是足夠的似乎只有你可以回答 –

1

你在找什麼是"fire and forget" - 這是inherently dangerous on ASP.NET

正確解決方案是使用可靠隊列(Azure隊列/ MSMQ)連接到WebAPI的獨立工作進程(Azure功能/ Win32服務)。您的WebAPI應該寫入隊列,然後返回響應。工作進程(ASP.NET外部)應從隊列中讀取並處理工作項目。

相關問題