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

本文关键字:任务 之间 区别 是什么 变量 WhenAll foreach Task | 更新日期: 2023-09-27 18:32:39

经过几个小时的挣扎,我在我的应用程序中发现了一个错误。我认为下面的 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
}

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

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

如果使用WhenAll并且任何项目都失败,则在所有项目完成之前,它仍然不会完成,并且它将表示包装所有任务的所有错误的AggregatException

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


两者的不同之处在于,WhenAll将在一开始就实现整个IEnumerable,然后再向其他项目添加任何延续。 如果 IEnumerable 表示已经存在和已启动任务的集合,则这无关紧要,但如果迭代可枚举的任务创建和/或启动任务,则在开始时具体化序列将并行运行它们,并且在获取下一个任务之前等待每个任务将按顺序执行它们。 下面是您可以传入的IEnumerable,其行为与我在此处描述的那样:

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

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

例如,如果任务使用相同的 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需要多长时间,使用foreach,您始终可以打印1,2,3,4,5。但是有了Task.WhenAll它们会并发执行并按完成顺序打印。