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 它们是同时执行的,并按完成顺序打印。