Task.WhenAll()和foreach(任务中的var任务)之间的区别是什么

经过几个小时的挣扎,我在我的应用程序中发现了一个错误。 我认为下面的两个函数具有相同的行为,但事实certificate它们没有。

任何人都可以告诉我幕后真的发生了什么,以及他们为什么会以不同的方式行事?

public async Task MyFunction1(IEnumerable tasks){ await Task.WhenAll(tasks); Console.WriteLine("all done"); // happens AFTER all tasks are finished } public async Task MyFunction2(IEnumerable tasks){ foreach(var task in tasks){ await task; } Console.WriteLine("all done"); // happens BEFORE all tasks are finished } 

如果所有任务都成功完成,它们的function相同。

如果您使用WhenAll并且任何项目都失败,则在完成所有项目之后仍然无法完成,并且它将表示包含所有任务中的所有错误的AggregatException

如果你await每一个,那么一旦它碰到任何失败的项目它就会完成它,它将代表一个错误的例外,而不是任何其他错误。


两者的不同之处还在于,在向其他项添加任何延续之前, WhenAll将在开始时实现整个IEnumerable权限。 如果IEnumerable表示已存在和已启动任务的集合,那么这是不相关的,但如果迭代可枚举的行为创建和/或启动任务,那么在开始时实现序列将并行运行它们,并且在获取下一个任务之前等待每个任务将按顺序执行它们。 下面是一个你可以传入的IEnumerable ,就像我在这里描述的那样:

 public static IEnumerable TaskGeneratorSequence() { for(int i = 0; i < 10; i++) yield return Task.Delay(TimeSpan.FromSeconds(2); } 

可能最重要的function差异是Task.WhenAll可以在您的任务执行真正的异步操作(例如IO)时引入并发性。 根据您的情况,这可能是您想要的,也可能不是。

例如,如果您的任务使用相同的EF DbContext查询数据库,则下一个查询将在第一个查询“飞行中”时立即触发,这会导致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需要多长时间,你都可以获得1,2,3,4,5的打印效果。 但是使用Task.WhenAll它们会同时执行并按完成顺序打印。