ChatGPT解决这个技术问题 Extra ChatGPT

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

如果我不关心任务完成的顺序,只需要它们全部完成,我是否还应该使用 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();
        }
    }
}
如果您实际上不知道需要并行执行多少任务怎么办?如果您有 1000 个任务需要运行怎么办?第一个将不太可读 await t1; await t2; ....; await tn =>在这两种情况下,第二个始终是最佳选择
你的评论很有道理。我只是想为自己澄清一些事情,这与我最近answered的另一个问题有关。在这种情况下,有 3 个任务。

u
usr

是的,请使用 WhenAll,因为它会同时传播所有错误。使用多个等待,如果前面的等待之一抛出,您将丢失错误。

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

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


“因为它会立即传播所有错误”如果您await其结果则不会。
至于如何使用 Task 管理异常的问题,这篇文章对它背后的推理给出了一个快速但很好的见解(而且恰好也顺便记下了 WhenAll 与多重等待相比的好处):{ 1}
@OskarLindberg OP 正在等待第一个任务之前开始所有任务。所以它们同时运行。感谢您的链接。
@usr我仍然很想知道WhenAll是否没有做一些聪明的事情,比如保存相同的SynchronizationContext,以进一步推动其除了语义之外的好处。我没有找到确凿的文档,但是查看 IL 显然有不同的 IAsyncStateMachine 实现在起作用。我没有很好地阅读 IL,但WhenAll 至少似乎可以生成更高效的 IL 代码。 (无论如何,仅凭 WhenAll 的结果反映了我所涉及的所有任务的状态这一事实就足以在大多数情况下更喜欢它。)
另一个重要区别是WhenAll 将等待所有任务完成,即使例如t1 或t2 抛出异常或被取消。
M
Marcel Popescu

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

从给定的上下文开始

保存上下文

等待t1

恢复原始上下文

保存上下文

等t2

恢复原始上下文

保存上下文

等t3

恢复原始上下文

相比之下,DoWork2 这样做:

从给定的上下文开始

保存上下文

等待所有 t1、t2 和 t3

恢复原始上下文

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


您似乎认为向同步上下文发送消息很昂贵。真的不是。您有一个委托被添加到队列中,该队列将被读取并执行委托。这增加的开销确实非常小。这不是什么,但也不是很大。在几乎所有情况下,无论异步操作是什么,开销都会相形见绌。
同意,这只是我能想到的偏爱其中一个的唯一原因。好吧,再加上与 Task.WaitAll 的相似性,线程切换的成本更高。
@Servy 正如马塞尔指出的那样,这真的取决于。例如,如果您在所有 db 任务上使用 await 作为原则问题,并且该 db 与 asp.net 实例位于同一台机器上,则在某些情况下您将等待内存中索引的 db 命中,更便宜比那个同步开关和线程池洗牌。在这种情况下,使用 WhenAll() 可能会取得重大的整体胜利,所以......这真的取决于。
@ChrisMoschini 数据库查询,即使它与服务器位于同一台机器上的数据库,也不会比向消息泵添加一些代表的开销更快。内存中的查询仍然几乎肯定会慢很多。
另请注意,如果 t1 较慢而 t2 和 t3 较快 - 然后另一个等待立即返回。
L
Lukazoid

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

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

M
Maverick Meerkat

(免责声明:此答案取自/启发自 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 任务之后添加一个空的延续并将 await 放在那里。这样,如果任务失败,结果属性将抛出完整的聚合异常:

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

r
rarrarrarrr

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


S
Sohail Shaghasi

就这么简单。

如果您对外部 api 或数据库有多个 http 调用 IEnumerable,请使用 WhenAll并行执行请求,而不是等待单个调用完成然后继续其他调用。


问题根本与http无关
您好,欢迎来到本站。如果 OP 在调用 taskB 之前等待 taskA,这可能是正确的,但他们首先启动了所有任务。