[英]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);
}
這種“帶有異步回退的同步路徑”方法越來越普遍,尤其是在同步完成相對頻繁的高性能代碼中。 請注意,如果完成總是真正異步的,它根本沒有幫助。
此處適用的其他事項:
在最近的 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); }
如果有很多不同的返回值完全同步,那么更喜歡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); }
如果可能,比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 種可能的情況:
當 FeedCat 失敗時,SellHouse 已經成功完成。 在這種情況下,你很好。
SellHouse 不完整,並且在某些時候出現異常而失敗。 未觀察到異常並將在終結器線程上重新拋出。
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.WhenAll
或Task.WaitAll
,具體取決於您是否希望線程等待。 查看鏈接以了解兩者的解釋。
前方警告
只是快速提醒那些訪問該線程和其他類似線程以尋找一種使用 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.
您示例中的三個任務的重要性差異很大。 萬一其中一個失敗,您可能想知道其他人發生了什么。 比如與自動喂貓器通訊失敗,你不想錯過賣房成功還是失敗。 因此,不僅要返回Cat
、 House
和Tesla
,還要返回任務本身。 然后調用代碼將能夠分別查詢三個任務中的每一個,並對它們的成功或失敗完成做出適當的反應:
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.");
帶有空catch
的try
塊是必需的,因為 .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.