我有 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;
Task.WhenAll
实际上不会以任何可观察的方式改变程序的行为。这是一个纯粹的冗余方法调用。如果您愿意,欢迎您添加它作为审美选择,但它不会改变代码的功能。无论是否调用该方法,代码的执行时间都将相同(好吧,从技术上讲,调用 WhenAll
会有非常小的开销,但这应该可以忽略不计),只是使版本比此版本运行时间略长。
WhenAll
是纯粹的美学变化。唯一可观察到的行为差异是,如果较早的任务出错,您是否等待后面的任务完成,这通常不需要这样做。如果您不相信关于为什么您的陈述不正确的众多解释,您可以简单地自己运行代码并查看它是否不正确。
如果您使用的是 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 和已完成任务的一些优化。
Task.WhenAll()
没有返回元组。在 Task.WhenAll()
返回的任务完成后,从所提供任务的 Result
属性构造一个。
.Result
调用,以避免其他人通过复制您的示例来延续这种不良做法。
给定三个任务 - 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#,一个常见的模式是异步回退方法通常实现为本地函数: Task
Task
。
Task.WhenAll
没有区别。但我确实在每次迭代中看到 Task.WhenAll
和 await 之间的明显区别。如果我创建 10 个等待时间为 500 毫秒的等待并将它们与 Task.WhenAll
一起启动,它们会在不到一秒的时间内完成。而如果我等待每 10 个等待 - 它们是按顺序执行的(正如我所期望的)并在约 5 秒内完成。
您可以将它们存储在任务中,然后等待它们:
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;
var catTask = FeedCat()
不执行函数 FeedCat()
并将结果存储到 catTask
使 await Task.WhenAll()
部分无用,因为该方法已经执行?
如果您尝试记录所有错误,请确保在代码中保留 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 将在其中完成一些等待时立即失败。发生这种情况是因为您基本上发出了 fire & forget 调用,并且一旦 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 未观察到的异常会使您的进程崩溃。
更多详细信息:Task Exception Handling in .NET 4.5
前向警告
只是对那些访问此线程和其他类似线程的人的快速提醒,他们正在寻找一种使用 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.
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());
如果你想访问 Cat,你可以这样做:
var ct = (Cat)dn[0];
这做起来非常简单,使用起来也非常有用,无需追求复杂的解决方案。
dynamic
是魔鬼。它用于棘手的 COM 互操作等,不应在任何非绝对需要的情况下使用。特别是如果您关心性能。或键入安全性。或者重构。或者调试。
不是等待语句使代码按顺序运行吗?考虑以下代码
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!
WhenAll
;等待将确保您在任务全部完成之前不会超过后面的 3 个任务。Task.WhenAll()
允许以并行模式运行任务。我不明白为什么@Servy 建议删除它。如果没有WhenAll
,它们将一一运行catTask
从FeedCat
返回时已经在运行。因此,任何一种方法都行得通 - 唯一的问题是您是想一次await
一个还是一起await
。错误处理稍有不同 - 如果您使用Task.WhenAll
,那么它会将它们全部await
,即使其中一个提前失败。WhenAll
对操作执行的时间或执行方式没有影响。它只有有任何可能性影响结果的观察方式。在这种特殊情况下,唯一的区别是前两种方法之一中的错误会导致在我的方法中比斯蒂芬更早地在此调用堆栈中抛出异常(尽管总是会抛出相同的错误,如果有的话)。