WhenAny和WhenAll与WaitAll和none的区别,假设结果会被立即使用。

8

在多个异步任务完成后,我必须立即消耗它们的输出。

在这些方法中,是否存在合理的性能差异?

简单等待

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (Task<List<Baz>> task in tasks) {
        results.AddRange(await task);

    return results;
}

WhenAll

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (List<Baz> bazList in await Task.WhenAll(tasks))
        results.AddRange(bazList);

    return results;
}

WaitAll

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (List<Baz> bazList in await Task.WaitAll(tasks))
        results.AddRange(bazList);

    return results;
}

WhenAny

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    while (tasks.Count > 0) {
        Task<List<Baz>> finished = Task.WhenAny(tasks);
        results.AddRange(await finished);
        tasks.Remove(finished);
    }

    return results;
}
  • FooList 有约100个条目。
  • FetchBazListFromFoo 对大约30个REST API进行调用,并对每个REST API调用的结果进行一些同步工作。

另外,WhenAll和WhenAny之间是否存在内部开销差异?

当所有任务完成后,WhenAll返回控制权,而当任意一个任务完成时,WhenAny返回控制权。后者似乎需要更多的内部管理。

2个回答

8
第三种方法(WaitAll)无效,因为Task.WaitAll是一个返回void的方法,所以它不能被等待。这段代码只会产生编译时错误。
其他三种方法非常相似,但有一些微妙的差别。
简单等待:启动所有任务,然后逐个等待它们完成。它将按正确的顺序收集所有结果。如果发生异常,则会在所有任务完成之前返回,并且仅报告第一个失败任务(按顺序而非时间顺序)的异常。
除非您确切需要此行为(很可能不是),否则不建议使用。
WhenAll:启动所有任务,然后等待它们全部完成。它将按正确的顺序收集所有结果。如果发生异常,则会在所有任务完成后返回,并且仅报告第一个失败任务(按顺序而非时间顺序)的异常。
除非您确切需要此行为(很可能也不是),否则不建议使用。

WhenAny:启动所有任务,然后等待它们全部完成。它将按照完成的顺序收集所有结果,因此原始顺序将不会被保留。如果出现异常,它将立即返回,并报告第一个失败任务的异常(这次是按时间顺序第一个,而非按顺序)。while循环引入了一个开销,其他两种方法没有,如果任务数大于10,000,这种开销将相当显著,随着任务数量的增加,它将呈指数级增长。
除非您确切需要此行为(我敢打赌到现在你也应该不喜欢它)。

所有这些方法:都会向远程服务器发出大量并发请求,使其难以快速响应,在最坏的情况下触发防御性反DOS攻击机制。

解决这个问题的更好方法是使用专用 API Parallel.ForEachAsync,可从 .NET 6 及更高版本中使用。该方法并行化多个异步操作,强制执行默认的最大并行度 Environment.ProcessorCount,还支持取消和快速完成(在出现异常的情况下)。您可以在此处找到一个使用示例here。该方法不返回异步操作的结果。您可以将结果作为异步操作的副作用进行收集,如此处所示here

另一个更高级的解决方案是 TPL Dataflow 库。可以在 这里 找到该库的使用示例。


谢谢您的回答。我已经与服务器达成了协议,并确信工作不会被限制。一定会研究TPL。 - Nishant
Await、WhenAll和WhenAny不会启动任务。任务通常是热启动的(唯一一个任务没有运行的情况是当它是通过new Task创建的,但这并不是创建任务的推荐方式)。Await、WhenAll和WhenAny只是等待一个或多个任务完成。 - ckuri
@ckuri,我所提到的方法是OP所称的“简单等待”、“WhenAll”和“WhenAny”。我并不是在指关键字/方法awaitTask.WhenAllTask.WhenAny本身。这些方法包括创建(可能是热的)任务,正如你可以看到嵌入在问题代码中的那样。 - Theodor Zoulias
我认为WhenAny的第一句话应该是:启动所有任务,然后等待它们中的任意一个完成。看起来像是复制粘贴错误。 - undefined
@MustafaÖzçetin 我在回答中提到的"WhenAny"并不对应于Task.WhenAny方法。它对应于问题正文中的第四种方法,标题为WhenAny(以粗体显示)。 - undefined

2
简单的等待将按顺序执行每个项目,基本上是同步的 - 这将是最慢的。
当所有任务完成后,WhenAll 将等待所有任务完成 - 运行时间将是最长的单个任务所需的时间。
不要使用 WaitAll - 它是同步的,只需使用 WhenAllWhenAny 允许您在每个任务完成时处理它。这比某些情况下的 WhenAll 更快,具体取决于任务完成后需要处理多少内容。
在我看来,除非您需要在每个任务完成时立即开始后期处理,否则 WhenAll 是最简单/最清晰的方法,在大多数情况下都可以正常工作。

不应该等待遇到阻塞调用(在这种情况下是 REST API 调用)就将控制权返回给调用方吗?一旦控制权返回,控制应该继续执行其他的 await 调用。 - Nishant
在WhenAll和WhenAny中是否存在内部开销差异?在WhenAll中,控制将在所有操作完成后返回,而在WhenAny中,控制可能会在其中一个操作完成后立即返回。后者似乎需要更多的内部管理。 - Nishant
我的假设是WhenAny可能会有更多的开销,即使只是稍微的,但这完全是我猜测的。 await实际上是代码的一个停止点,在异步操作完成之前它将会等待。 - Tyler Hundley
根据您需要进行的调用次数,值得考虑一下异步可枚举。使用WhanAny是明智的选择。 - Tyler Hundley
1
WhenAll不会启动任务,因为任务通常已经在运行 - 这里是FetchBazListFromFoo。它只是等待任务完成。这意味着即使使用简单的await all,所有任务也会同时运行,因为这些任务是在等待一个任务完成之前创建的。连续等待每个任务和使用Task.WhenAll之间唯一的区别在于异常发生的情况下,WhenAll仍然会等待所有任务完成,而简单的等待则不是这种情况。 - ckuri
如果需要立即使用结果,Microsoft有一篇有用的博客文章推荐使用while循环的WhenAny方法:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/start-multiple-async-tasks-and-process-them-as-they-complete - GameSalutes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接