.net 为什么我更喜欢单一的'等待任务.WhenAll'而不是多个等待?

lstz6jyr  于 2022-12-30  发布在  .NET
关注(0)|答案(7)|浏览(298)

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

kqlmhetl1#

是的,使用WhenAll,因为它一次传播所有错误。使用多个等待,如果较早的等待之一抛出,则会丢失错误。
另一个重要的区别是WhenAll将等待所有任务完成 * 即使出现故障 *(出错或取消的任务)。按顺序手动等待会导致意外的并发,因为程序中需要等待的部分实际上会提前继续。
我认为它还使阅读代码更容易,因为您需要的语义直接记录在代码中。

dy2hfwbg

dy2hfwbg2#

我的理解是,与多个await相比,更喜欢Task.WhenAll的主要原因是性能/任务的“搅动”:DoWork1方法的作用类似于:

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

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

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

当然,对于您的特定情况,这是否是一个足够大的问题是“上下文相关的”(请原谅双关语)。

6tqwzwtp

6tqwzwtp3#

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

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

vwkv1x7d4#

(免责声明:此答案取自Ian Griffiths在Pluralsight上的TPL Async课程/受其启发)
喜欢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;等等。随后的任务异常将不会被观察到。
WhenAll中的情况-如果任何或所有任务出错,结果任务将包含所有异常。await关键字仍然总是重新抛出第一个异常。因此,其他异常仍然有效地未被观察到。克服这一问题的一种方法是在任务WhenAll之后添加一个空continuation,并将await放在那里。这样,如果任务失败,result属性将抛出完整的聚合异常:

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

p1tboqfb5#

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

mwyxok5s

mwyxok5s6#

事情就是这么简单。
如果你有多个对外部API或数据库的http调用IEnumerable,使用WhenAll并行执行请求,而不是等待一个调用完成后再继续其他的。

zbwhf8kr

zbwhf8kr7#

TL&DR:任务。WhenAll使您能够更有效地利用所有线程池,因为它们是同时运行的。当您执行await x,await y,await z时,这是串行运行的。

相关问题