为什么我要在多个等待中选择一个“等待任务.WhenAll”?


126

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

2
如果您实际上不知道需要并行执行多少个任务怎么办?如果您需要运行1000个任务怎么办?第一个将不太可读await t1; await t2; ....; await tn=>第二个总是这两种情况下的最佳选择
cuongle 2013年

您的评论很有道理。我只是想为自己澄清一些事情,这与我最近回答的另一个问题有关。在这种情况下,有3个任务。
13年

Answers:


112

是的,请使用,WhenAll因为它可以一次传播所有错误。如果有多个等待,则如果较早的等待之一抛出,则会丢失错误。

另一个重要的区别是,即使存在故障(故障或取消的任务),WhenAll也将等待所有任务完成。依次手动等待会导致意外的并发,因为要等待的程序部分实际上会提前进行。

我认为这也使阅读代码更加容易,因为所需的语义已直接记录在代码中。


9
“因为它会立即传播所有错误”,如果得到await的结果不是。
svick

2
至于如何使用Task管理异常的问题,本文提供了快速而深入的见解,以了解其背后的原因(恰好也恰好也使人注意到了WhenAll与多次等待相比的好处):博客.msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg OP在等待第一个任务之前就开始了所有任务。因此它们可以同时运行。感谢您的链接。
usr

3
@usr我仍然很好奇,仍然想知道WhenAll是否不会做一些聪明的事情,例如保存相同的SynchronizationContext,以进一步将其好处推到语义之外。我找不到确定性的文档,但是从IL来看,显然有一些IAsyncStateMachine的实现方式在起作用。我不太了解IL,但是,WhenAll至少看起来会生成更有效的IL代码。(无论如何,WhenAll的结果反映了我所涉及的所有任务的状态这一事实,在大多数情况下都是有理由偏爱它的原因。)
Oskar Lindberg

17
另一个重要的区别是,即使t1或t2抛出异常或被取消,WhenAll也会等待所有任务完成。
Magnus 2014年

28

我的理解是,更喜欢Task.WhenAll使用awaits的主要原因是性能/任务“搅动”:该DoWork1方法执行以下操作:

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

相比之下,DoWork2这样做:

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

当然,这对于您的特定情况是否足够大,取决于“上下文相关”(请原谅)。


4
您似乎认为向同步上下文发送消息非常昂贵。真的不是。您有一个添加到队列中的委托,该队列将被读取并执行委托。老实说,这增加的开销很小。这不是没有,但也不大。在几乎所有情况下,无论异步操作是什么,其开销都将使此类开销相形见war。
Servy

同意,这只是我想到一个比另一个更喜欢的唯一原因。好吧,再加上与Task.WaitAll的相似之处,其中线程切换的成本更高。
Marcel Popescu 2013年

1
正如马塞尔(Marcel)指出的那样,@ Servy确实取决于。例如,如果原则上在所有数据库任务上使用await,并且该数据库与asp.net实例位于同一台计算机上,那么在某些情况下,您将等待数据库命中的内存索引,价格便宜而不是同步开关和线程池改组。在这种情况下,WhenAll()可能会取得重大的整体胜利,所以...这确实取决于。
克里斯·莫斯基尼

3
@ChrisMoschini数据库查询即使击中了与服务器位于同一台机器上的数据库,也无法比向消息泵添加几个委托的开销更快。该内存中查询仍然几乎肯定会慢很多。
2015年

另请注意,如果t1较慢,t2和t3较快-则另一个等待立即返回。
David Refaeli,

18

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

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

7

(免责声明:此答案取自/启发自Ian Griffiths的TPL异步课程,了解Pluralsight

选择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个任务都抛出异常,则仅捕获第一个任务。以后的任何异常都将丢失。即,如果t2和t3引发异常,则仅捕获t2;否则,将仅捕获t2。等等。后续任务异常将不会被观察到。

与whenAll一样-如果任何或所有任务出错,则生成的任务将包含所有异常。关键字await仍然始终重新引发第一个异常。因此,其他例外仍然有效地未被发现。解决此问题的一种方法是在WhenAll任务之后添加一个空的延续,并在其中放置等待。这样,如果任务失败,则result属性将引发完整的Aggregate Exception:

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

6

这个问题的其他答案提供了技术原因 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);它更具可维护性,因为它可以更清楚地传达您的意图,并且不易受到特有错误的影响,这些错误可能是源于对代码的善意更新,甚至是合并出错。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.