为什么我应该更喜欢单个“await Task.WhenAll”而不是多个等待?

问题描述 投票:0回答:7

如果我不关心任务完成的顺序,只需要全部完成,我是否仍然应该使用

await Task.WhenAll
而不是多个
await
?例如,
DoWork2
是否低于
DoWork1
的首选方法(为什么?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}
c# .net parallel-processing task-parallel-library async-await
7个回答
180
投票

是的,使用

WhenAll
,因为它会立即传播所有错误。通过多个等待,如果较早的等待之一抛出,您就会丢失错误。

另一个重要的区别是

WhenAll
将等待所有任务完成即使存在失败(出现故障或取消的任务)。按顺序手动等待会导致意外的并发,因为程序中想要等待的部分实际上会提前继续。

我认为这也使阅读代码变得更容易,因为您想要的语义直接记录在代码中。


37
投票

我的理解是,更喜欢

Task.WhenAll
而不是多个
await
的主要原因是性能/任务“搅动”:
DoWork1
方法执行以下操作:

  • 从给定的上下文
  • 开始
  • 保存上下文
  • 等待t1
  • 恢复原始上下文
  • 保存上下文
  • 等待t2
  • 恢复原始上下文
  • 保存上下文
  • 等待t3
  • 恢复原始上下文

相比之下,

DoWork2
这样做:

  • 从给定的上下文开始
  • 保存上下文
  • 等待所有 t1、t2 和 t3
  • 恢复原始上下文

这对于您的具体情况是否足够重要,当然,“取决于上下文”(请原谅双关语)。


23
投票

异步方法作为状态机实现。可以编写方法以使它们不被编译到状态机中,这通常称为快速异步方法。这些可以像这样实现:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

使用

Task.WhenAll
时,可以维护此快速跟踪代码,同时仍然确保调用者能够等待所有任务完成,例如:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

12
投票

(免责声明:此答案取自/启发自 Ian Griffiths 在 Pluralsight 上的 TPL 异步课程)

选择 WhenAll 的另一个原因是异常处理。

假设您的 DoWork 方法上有一个 try-catch 块,并假设它们调用不同的 DoTask 方法:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

在这种情况下,如果 3 个任务都抛出异常,则只有第一个任务会被捕获。任何稍后的异常都将丢失。 IE。如果 t2 和 t3 抛出异常,则只有 t2 会被捕获;等等。后续任务异常将不会被观察到。

与 WhenAll 中一样 - 如果任何或所有任务出现故障,则生成的任务将包含所有异常。 wait 关键字仍然总是重新抛出第一个异常。因此,其他异常实际上仍然未被观察到。解决这个问题的一种方法是在任务 WhenAll 之后添加一个空的延续并将等待放在那里。这样,如果任务失败,结果属性将抛出完整的聚合异常:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

10
投票

此问题的其他答案提供了首选

await Task.WhenAll(t1, t2, t3);
的技术原因。 这个答案旨在从更温和的一面(@usr 提到的)来看待它,同时仍然得出相同的结论。

await Task.WhenAll(t1, t2, t3);
是一种更实用的方法,因为它声明意图并且是原子的。

使用

await t1; await t2; await t3;
,没有什么可以阻止队友(甚至可能是未来的你!)在各个
await
语句之间添加代码。 当然,您已将其压缩为一行以基本上实现这一目标,但这并不能解决问题。 此外,在团队环境中,在给定代码行上包含多个语句通常是不好的形式,因为它会使人眼更难扫描源文件。

简单地说,

await Task.WhenAll(t1, t2, t3);
更易于维护,因为它更清楚地传达您的意图,并且不易受到特殊错误的影响,这些错误可能是出于善意的代码更新,甚至只是合并出错。


0
投票

方法

DoWork1
await t1; await t2; await t3;
启动
t1
并等待它完成,然后启动任务
t2
等等。

如果

t1
抛出异常并且任务
t2
t3
甚至没有启动。

方法

DoWork2
同时启动所有3个任务并并行执行。 t1
 中的异常不会停止处理 
t2
t3

简单的答案是:

WhenAll

await t1; await t2;...
都是正确的(语法和语义),何时使用其中之一取决于您的用例。


-2
投票
就这么简单。

如果您对外部 api 或数据库有多个 http 调用

IEnumerable

,请使用 
WhenAll
 
并行执行请求,而不是等待单个调用完成然后继续其他调用。

© www.soinside.com 2019 - 2024. All rights reserved.