簡體   English   中英

等待具有不同結果的多個任務

[英]Awaiting multiple Tasks with different results

我有3個任務:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

它們都需要在我的代碼可以繼續之前運行,我也需要每個結果。 結果之間沒有任何共同點

如何調用並等待 3 個任務完成然后得到結果?

使用WhenAll ,您可以使用await單獨提取結果:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

您還可以使用Task.Result (因為您知道此時它們都已成功完成)。 但是,我建議使用await因為它顯然是正確的,而Result在其他情況下可能會導致問題。

只需在啟動它們后分別await三個任務。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

如果您使用的是 C# 7,則可以使用這樣一個方便的包裝器方法...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

...當您想要等待具有不同返回類型的多個任務時,啟用這樣的方便語法。 當然,您必須為要等待的不同數量的任務進行多次重載。

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

但是,如果您打算將此示例變成真實的示例,請參閱 Marc Gravell 的回答,了解有關 ValueTask 和已完成任務的一些優化。

給定三個任務—— FeedCat()SellHouse()BuyCar() ,有兩種有趣的情況:要么它們都同步完成(出於某種原因,可能是緩存或錯誤),要么不同步。

假設我們有,從問題:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

現在,一個簡單的方法是:

Task.WhenAll(x, y, z);

但是……這不便於處理結果; 我們通常希望await

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

但這會產生大量開銷並分配各種數組(包括params Task[]數組)和列表(內部)。 它有效,但不是很好的 IMO。 在許多方面,使用async操作並依次await每個操作更簡單

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

相反,一些上述評論的,使用await代替Task.WhenAll使得該任務的運行方式(同時,順序等)沒有什么區別 在最高級別, Task.WhenAll早於async / await良好編譯器支持,並且在這些東西不存在時很有用。 當您有一組任意任務而不是 3 個謹慎的任務時,它也很有用。

但是:我們仍然存在async / await為繼續生成大量編譯器噪音的問題。 如果任務可能實際上同步完成,那么我們可以通過在具有異步回退的同步路徑中構建來優化它:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

這種“帶有異步回退的同步路徑”方​​法越來越普遍,尤其是在同步完成相對頻繁的高性能代碼中。 請注意,如果完成總是真正異步的,它根本沒有幫助。

此處適用的其他事項:

  1. 在最近的 C# 中,一個常見的模式是async回退方法通常作為本地函數實現:

     Task<string> DoTheThings() { async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) { return DoWhatever(await a, await b, await c); } Task<Cat> x = FeedCat(); Task<House> y = SellHouse(); Task<Tesla> z = BuyCar(); if(x.Status == TaskStatus.RanToCompletion && y.Status == TaskStatus.RanToCompletion && z.Status == TaskStatus.RanToCompletion) return Task.FromResult( DoWhatever(a.Result, b.Result, c.Result)); // we can safely access .Result, as they are known // to be ran-to-completion return Awaited(x, y, z); }
  2. 如果有很多不同的返回值完全同步,那么更喜歡ValueTask<T>Task<T>

     ValueTask<string> DoTheThings() { async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) { return DoWhatever(await a, await b, await c); } ValueTask<Cat> x = FeedCat(); ValueTask<House> y = SellHouse(); ValueTask<Tesla> z = BuyCar(); if(x.IsCompletedSuccessfully && y.IsCompletedSuccessfully && z.IsCompletedSuccessfully) return new ValueTask<string>( DoWhatever(a.Result, b.Result, c.Result)); // we can safely access .Result, as they are known // to be ran-to-completion return Awaited(x, y, z); }
  3. 如果可能,比Status == TaskStatus.RanToCompletion更喜歡IsCompletedSuccessfully 這現在存在於 .NET Core 的Task ,以及無處不在的ValueTask<T>

您可以將它們存儲在任務中,然后等待它們:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;

如果您嘗試記錄所有錯誤,請確保在代碼中保留 Task.WhenAll 行,很多評論建議您可以將其刪除並等待單個任務。 Task.WhenAll 對於錯誤處理非常重要。 如果沒有這一行,您可能會為未觀察到的異常打開代碼。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

想象一下 FeedCat 在以下代碼中拋出異常:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

在這種情況下,您將永遠不會等待 houseTask 或 carTask。 這里有 3 種可能的情況:

  1. 當 FeedCat 失敗時,SellHouse 已經成功完成。 在這種情況下,你很好。

  2. SellHouse 不完整,並且在某些時候出現異常而失敗。 未觀察到異常並將在終結器線程上重新拋出。

  3. SellHouse 不完整,其中包含等待。 如果您的代碼在 ASP.NET SellHouse 中運行,一旦其中的一些等待完成,它就會失敗。 發生這種情況是因為您基本上觸發並忘記調用,並且一旦 FeedCat 失敗,同步上下文就丟失了。

這是您將在案例 (3) 中得到的錯誤:

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

對於情況 (2),您將收到類似的錯誤,但具有原始異常堆棧跟蹤。

對於 .NET 4.0 及更高版本,您可以使用 TaskScheduler.UnobservedTaskException 捕獲未觀察到的異常。 對於 .NET 4.5 及更高版本,默認情況下會吞下未觀察到的異常,因為 .NET 4.0 未觀察到的異常將使您的進程崩潰。

此處有更多詳細信息: .NET 4.5 中的任務異常處理

您可以使用Task.WhenAll提到的Task.WhenAllTask.WaitAll ,具體取決於您是否希望線程等待。 查看鏈接以了解兩者的解釋。

WaitAll vs WhenAll

前方警告

只是快速提醒那些訪問該線程和其他類似線程以尋找一種使用 async+await+task 工具集並行化 EntityFramework 的方法:此處顯示的模式是合理的,但是,當涉及到 EF 的特殊雪花時,您將不會實現並行執行,除非並且直到您在涉及的每個 *Async() 調用中使用單獨的(新的)db-context-instance。

由於 ef-db-contexts 的固有設計限制,禁止在同一個 ef-db-context 實例中並行運行多個查詢,因此這種事情是必要的。


利用已經給出的答案,即使在一個或多個任務導致異常的情況下,這是確保收集所有值的方法:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

具有或多或少相同性能特征的替代實現可能是:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }

使用Task.WhenAll然后等待結果:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.

您示例中的三個任務的重要性差異很大。 萬一其中一個失敗,您可能想知道其他人發生了什么。 比如與自動喂貓器通訊失敗,你不想錯過賣房成功還是失敗。 因此,不僅要返回CatHouseTesla ,還要返回任務本身。 然后調用代碼將能夠分別查詢三個任務中的每一個,並對它們的成功或失敗完成做出適當的反應:

public async Task<(Task<Cat>, Task<House>, Task<Tesla>)> FeedCatSellHouseBuyCar()
{
    Task<Cat> task1 = FeedCat();
    Task<House> task2 = SellHouse();
    Task<Tesla> task3 = BuyCar();

    // All three tasks are launched at this point.

    try { await Task.WhenAll(task1, task2, task3).ConfigureAwait(false); } catch { }

    // All three tasks are completed at this point.
    
    return (task1, task2, task3);
}

使用示例:

var (catTask, houseTask, teslaTask) = await FeedCatSellHouseBuyCar();

// All three tasks are completed at this point.

if (catTask.IsCompletedSuccessfully)
    Console.WriteLine($"{catTask.Result.Name} is eating her healthy meal.");
else
    Console.WriteLine("Your cat is starving!");

if (houseTask.IsCompletedSuccessfully)
    Console.WriteLine($"Your house at {houseTask.Result.Address} was sold. You are now rich and homeless!");
else
    Console.WriteLine("You are still the poor owner of your house.");

if (teslaTask.IsCompletedSuccessfully)
    Console.WriteLine($"You are now the owner a battery-powered {teslaTask.Result.Name}.");
else
    Console.WriteLine("You are still driving a Hyundai.");

帶有空catchtry塊是必需的,因為 .NET 7 仍然沒有提供正確的方法await任務而不在取消或失敗的情況下拋出。

var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

如果要訪問 Cat,請執行以下操作:

var ct = (Cat)dn[0];

這非常簡單,使用起來也非常有用,無需追求復雜的解決方案。

不是 await 語句使代碼按順序運行嗎? 考慮以下代碼

class Program
{
    static Stopwatch _stopwatch = new();

    static async Task Main(string[] args)
    {
        Console.WriteLine($"fire hot");
        _stopwatch.Start();
        var carTask = BuyCar();
        var catTask = FeedCat();
        var houseTask = SellHouse();
        await carTask;
        await catTask;
        await houseTask;
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} done!");

        Console.WriteLine($"using await");
        _stopwatch.Restart();
        await BuyCar();
        await FeedCat();
        await SellHouse();            

        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} done!");
    }

    static async Task BuyCar()
    {
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} buy car started");
        await Task.Delay(2000);
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} buy car done");
    }

    static async Task FeedCat()
    {
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} feed cat started");
        await Task.Delay(1000);
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} feed cat done");
    }

    static async Task SellHouse()
    {
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} sell house started");
        await Task.Delay(10);
        Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} sell house done");
    }
}

fire hot
0 buy car started
3 feed cat started
4 sell house started
18 sell house done
1004 feed cat done
2013 buy car done
2014 done!
using await
0 buy car started
2012 buy car done
2012 feed cat started
3018 feed cat done
3018 sell house started
3033 sell house done
3034 done!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM