Task.WhenAll() 和 foreach(var task in tasks) 有什么区别
What's the diference between Task.WhenAll() and foreach(var task in tasks)
经过几个小时的努力,我在我的应用程序中发现了一个错误。我认为下面的 2 个函数具有相同的行为,但事实证明它们没有。
谁能告诉我幕后到底发生了什么,为什么他们的行为方式不同?
public async Task MyFunction1(IEnumerable<Task> tasks){
await Task.WhenAll(tasks);
Console.WriteLine("all done"); // happens AFTER all tasks are finished
}
public async Task MyFunction2(IEnumerable<Task> tasks){
foreach(var task in tasks){
await task;
}
Console.WriteLine("all done"); // happens BEFORE all tasks are finished
}
如果所有任务都成功完成,它们的功能将相同。
如果你使用 WhenAll
并且任何项目都失败了,它仍然不会完成,直到所有项目都完成,它会代表一个 AggregatException
包裹 [=26= 所有个任务中的]all个错误。
如果你 await
每个人,那么它会在遇到 任何失败的 项目时立即完成,并且它将代表该 一个错误,没有其他错误。
两者的不同之处还在于 WhenAll
会在一开始就具体化整个 IEnumerable
,然后再向其他项目添加任何延续。如果 IEnumerable
表示已经存在和开始的任务的集合,那么这是不相关的,但是如果迭代可枚举的行为创建 and/or 开始任务,那么在开始时具体化序列将 运行 它们都是并行的,在获取下一个任务之前等待每个任务将按顺序执行它们。下面是一个 IEnumerable
你可以传入它,它的行为就像我在这里描述的那样:
public static IEnumerable<Task> TaskGeneratorSequence()
{
for(int i = 0; i < 10; i++)
yield return Task.Delay(TimeSpan.FromSeconds(2);
}
可能最重要的功能差异是 Task.WhenAll
可以在您的任务执行真正的异步操作(例如 IO)时引入并发。根据您的情况,这可能是您想要的,也可能不是您想要的。
例如,如果您的任务使用相同的 EF DbContext 查询数据库,则下一个查询将在第一个查询为 "in flight" 时立即触发,这会导致 EF 崩溃,因为它不支持使用相同上下文的多个同时查询。
那是因为您没有单独等待每个异步操作。您正在等待一个代表所有这些异步操作完成的任务。它们也可以按任何顺序完成。
但是,当您在 foreach
中单独等待每个任务时,您只会在当前任务完成时触发下一个任务,从而防止并发并确保串行执行。
演示此行为的简单示例:
async Task Main()
{
var tasks = new []{1, 2, 3, 4, 5}.Select(i => OperationAsync(i));
foreach(var t in tasks)
{
await t;
}
await Task.WhenAll(tasks);
}
static Random _rand = new Random();
public async Task OperationAsync(int number)
{
// simulate an asynchronous operation
// taking anywhere between 100 to 3000 milliseconds
await Task.Delay(_rand.Next(100, 3000));
Console.WriteLine(number);
}
您会发现,无论 OperationAsync
花费多长时间,foreach 总是打印 1、2、3、4、5。但是 Task.WhenAll
它们是同时执行的,并按完成顺序打印。
经过几个小时的努力,我在我的应用程序中发现了一个错误。我认为下面的 2 个函数具有相同的行为,但事实证明它们没有。
谁能告诉我幕后到底发生了什么,为什么他们的行为方式不同?
public async Task MyFunction1(IEnumerable<Task> tasks){
await Task.WhenAll(tasks);
Console.WriteLine("all done"); // happens AFTER all tasks are finished
}
public async Task MyFunction2(IEnumerable<Task> tasks){
foreach(var task in tasks){
await task;
}
Console.WriteLine("all done"); // happens BEFORE all tasks are finished
}
如果所有任务都成功完成,它们的功能将相同。
如果你使用 WhenAll
并且任何项目都失败了,它仍然不会完成,直到所有项目都完成,它会代表一个 AggregatException
包裹 [=26= 所有个任务中的]all个错误。
如果你 await
每个人,那么它会在遇到 任何失败的 项目时立即完成,并且它将代表该 一个错误,没有其他错误。
两者的不同之处还在于 WhenAll
会在一开始就具体化整个 IEnumerable
,然后再向其他项目添加任何延续。如果 IEnumerable
表示已经存在和开始的任务的集合,那么这是不相关的,但是如果迭代可枚举的行为创建 and/or 开始任务,那么在开始时具体化序列将 运行 它们都是并行的,在获取下一个任务之前等待每个任务将按顺序执行它们。下面是一个 IEnumerable
你可以传入它,它的行为就像我在这里描述的那样:
public static IEnumerable<Task> TaskGeneratorSequence()
{
for(int i = 0; i < 10; i++)
yield return Task.Delay(TimeSpan.FromSeconds(2);
}
可能最重要的功能差异是 Task.WhenAll
可以在您的任务执行真正的异步操作(例如 IO)时引入并发。根据您的情况,这可能是您想要的,也可能不是您想要的。
例如,如果您的任务使用相同的 EF DbContext 查询数据库,则下一个查询将在第一个查询为 "in flight" 时立即触发,这会导致 EF 崩溃,因为它不支持使用相同上下文的多个同时查询。
那是因为您没有单独等待每个异步操作。您正在等待一个代表所有这些异步操作完成的任务。它们也可以按任何顺序完成。
但是,当您在 foreach
中单独等待每个任务时,您只会在当前任务完成时触发下一个任务,从而防止并发并确保串行执行。
演示此行为的简单示例:
async Task Main()
{
var tasks = new []{1, 2, 3, 4, 5}.Select(i => OperationAsync(i));
foreach(var t in tasks)
{
await t;
}
await Task.WhenAll(tasks);
}
static Random _rand = new Random();
public async Task OperationAsync(int number)
{
// simulate an asynchronous operation
// taking anywhere between 100 to 3000 milliseconds
await Task.Delay(_rand.Next(100, 3000));
Console.WriteLine(number);
}
您会发现,无论 OperationAsync
花费多长时间,foreach 总是打印 1、2、3、4、5。但是 Task.WhenAll
它们是同时执行的,并按完成顺序打印。