为什么我更喜欢单个'await Task.WhenAll'多次等待?

时间:2013-08-19 09:56:35

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

如果我不关心任务完成的顺序而只需要完成任务,我是否应该使用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();
        }
    }
}

5 个答案:

答案 0 :(得分:89)

是的,请使用WhenAll,因为它会一次传播所有错误。如果其中一个等待抛出,那么等待多个等待你就会失去错误。

另一个重要区别是WhenAll将等待所有任务完成。 await链会在第一个异常时中止等待,但会继续执行未等待的任务。这会导致意外的并发。

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

答案 1 :(得分:22)

我的理解是,选择Task.WhenAll到多个await的主要原因是性能/任务“搅拌”:DoWork1方法执行如下操作:

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

相比之下,DoWork2执行此操作:

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

对于你的特定情况,这是否足够大,当然是“依赖于上下文”(原谅双关语)。

答案 2 :(得分:14)

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

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);
}

答案 3 :(得分:3)

此问题的其他答案提供了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);更易于维护,因为它可以更清晰地传达您的意图,并且不易受到代码的良好更新所产生的特殊错误的影响,甚至只是合并错误。< / p>

答案 4 :(得分:3)

(免责声明:此答案来自Ian Griffiths&{39}上的TPL异步课程

更喜欢WhenAll是异常处理的另一个原因。

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

$('.carousel').carousel()

在这种情况下,如果所有3个任务都抛出异常,则只会捕获第一个异常。任何后来的异常都将丢失。即如果t2和t3抛出异常,则仅捕获t2;等后续任务异常将被忽略。

在WhenAll中 - 如果任何或所有任务发生故障,则生成的任务将包含所有异常。 await关键字仍然会重新抛出第一个异常。所以其他例外仍然是有效的观察。解决此问题的一种方法是在任务WhenAll之后添加一个空的延续并将等待放在那里。这样,如果任务失败,result属性将抛出完整的Aggregate Exception:

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)
    {
        // ...
    }

}