Task.WhenAll()和foreach(任务中的var任务)之间的差异是什么?

时间:2016-01-29 19:45:02

标签: c# .net async-await task-parallel-library task

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

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

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
}

2 个答案:

答案 0 :(得分:7)

如果所有任务成功完成,他们的功能相同

如果您使用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);
}

答案 1 :(得分:1)

最重要的功能差异是,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花费多长时间,对于foreach而言,始终会打印1,2,3,4,5。但是使用Task.WhenAll可以同时执行它们,并按其完成顺序打印。