【问题标题】:To use Task.WhenAll, or not to use Task.WhenAll使用Task.WhenAll,或不使用Task.WhenAll
【发布时间】:2021-05-14 19:58:22
【问题描述】:

我正在审查一些代码并试图提出一个技术原因,说明您应该或不应该使用Task.WhenAll(Tasks[]) 来实质上并行进行 Http 调用。 Http 调用调用不同的微服务,我猜其中一个调用可能需要也可能不需要一些时间来执行......(我想我对此并不感兴趣)。我正在使用 BenchmarkDotNet 让我了解是否有更多内存消耗,或者执行时间是否有很大不同。这是一个过于简化的基准示例:

[Benchmark]
public async Task<string> Task_WhenAll_Benchmark()
{
  var t1 = Task1();
  var t2 = Task2();

  await Task.WhenAll(t1, t2);

  return $"{t1.Result}===={t2.Result}";
}

[Benchmark]
public async Task<string> Task_KeepItSimple_Benchmark()
{
  return $"{await Task1()}===={await Task2()}";
}

Task1Task2 方法非常简单(我在类中有一个静态的HttpClient

public async Task<string> Task1()
{
  using (var request = await httpClient.GetAsync("http://localhost:8000/1.txt"))
  {
    return $"task{await request.Content.ReadAsStringAsync()}";
  }
}

public async Task<string> Task2()
{
  using (var request = await httpClient.GetAsync("http://localhost:8000/2.txt"))
  {
    return $"task{await request.Content.ReadAsStringAsync()}";
  }
}

我的结果

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Task_WhenAll_Benchmark 1.138 ms 0.0561 ms 0.1601 ms - - - 64 KB
Task_KeepItSimple_Benchmark 1.461 ms 0.0822 ms 0.2331 ms - - - 64 KB

如您所见,内存不是问题,执行时也没有太多时间。

我的问题确实是,是否有技术原因为什么您应该或不使用 Task.WhenAll()?这只是一种偏好吗?

我从 .net 核心团队的一个人那里遇到了Async Guidance,但它并没有真正涵盖这种情况。

编辑:这是 .net 框架 (4.6.1) 而不是核心!

编辑 2:更新以下评论中建议的基准之一。

我更新了 KeepItSimple 方法的基准...

[Benchmark]
public async Task<string> Task_KeepItSimple_Benchmark()
{
  var t1 = Task1();
  var t2 = Task2();

  return $"{await t1}===={await t2}";
}

并得到以下结果:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Task_WhenAll_Benchmark 1.134 ms 0.0566 ms 0.1613 ms - - - 64 KB
Task_KeepItSimple_Benchmark 1.081 ms 0.0377 ms 0.1070 ms - - - 64 KB

现在我更加困惑 - 执行速度如何更快(尽管数量很少!)?我以为代码的执行是在你await 结果时开始的......

【问题讨论】:

  • 使用Task.WhenAll(Tasks[])await t1应该没有太大区别;然后await t2;仅用于两个任务。如果你await t1t2t1 之前完成,那么await t2 只会遇到一个已完成的任务。当您增加任务数量时,差异会更加明显。
  • 不应该将Task_KeepItSimple_BenchmarkTask_WhenAll_Benchmark 中的var t1 = Task1(); var t2 = Task2(); 匹配以进行公平比较吗?在Task_KeepItSimple_Benchmark 中,您正在等待 task1 的所有内容,然后 task2 才能启动。
  • 同意,保持简单,目前按顺序执行它们,所以这不是一个真正有效的比较
  • I thought execution of the code started when you await the result - 一点也不。 method begins executing when it is calledawait(或“异步等待”)是调用代码(异步)等待方法完成的时间。
  • how is the execution faster...时间量和它们之间的差距是如此微小,以至于网络条件或服务器负载的轻微变化很容易导致这种情况,即使您使用的是本地服务器。您必须对其进行数十次测试才能开始获得任何有意义的平均值。

标签: c# async-await


【解决方案1】:

我的问题确实是,您是否应该使用 Task.WhenAll() 是否有技术原因?

两个调用都失败时的异常情况下的行为略有不同。如果他们一次是awaited,则永远不会观察到第二次失败;第一次失败的异常会立即传播。如果使用Task.WhenAll,则会观察到两种故障;两个任务都失败后传播一个异常。

这只是一种偏好吗?

这主要是偏好。我更喜欢WhenAll,因为代码更明确,但我对await一次一个没有问题。

【讨论】:

  • return $"{await Task1()}===={await Task2()}"; 是否会导致它们并行或顺序执行?
  • @ADyson 在您上面的评论中,顺序,但使用Task.WhenAll,等待将并行发生。由于对单个主机的 HTTP 请求顺序运行,建立连接的大部分开销将由第一个请求处理。请注意,您不能通过并行“无限扩大”,当您达到其他限制时,请求将开始排队。
  • @ADyson:好点;那时它们将是连续的。如果示例代码是return $"{await task1}===={await task2}";,我的回答是正确的
  • 正如我提出的问题,这是对实际代码所做工作的过度简化。实际上,结果存储在变量中并在代码的执行中进一步使用(因此我证明在我的返回中我要么await要么.Result异步调用)
【解决方案2】:

Task.WhenAll 只是creates a task that will complete when all of the supplied tasks have completed. 的一个方法而已。有一些优点和缺点,但其背后的总体想法是能够轻松管理大量任务。

当你开始你的任务时,它会被你的TaskScheduler安排,无论你是否使用它与讨论的方法。

好处是,如果您在循环中生成任务,例如,您不应该管理每一个任务。 例如

var i = 0;
var tasks = new List<Task>();
while(i++ < 10)
{
    var task = // start a task here
    tasks.Add(task);
}
var result = await Task.WhenAll(tasks);

您不应该等待所有这些,如果任何底层任务抛出异常,它将被重新抛出。

这就是我们面临某种复杂情况的地方,因为当您的异常被重新抛出时,它会被包装到 AggregateException 中,因此为了获得原始问题,您必须遍历它的 InnerExceptions 属性。但是,由于await 关键字解包AggregateException,您在等待WhenAll 时只会得到顶级异常,并且不会查看是否有其他问题。所以要得到AggregateException,你必须用一种旧的方式,用回调。但这是另一回事了。

顺便说一句,为了使您的第二个基准更接近您使用 WhenAll 方法所拥有的,您应该将其重构为:

[Benchmark]
public async Task<string> Task_KeepItSimple_Benchmark()
{
  var t1 = Task1();
  var t2 = Task2();

  return $"{await t1}===={await t2}";
}

【讨论】:

  • 我将更新我的基准并发布结果
【解决方案3】:

答案是:需要时使用。这与一个接一个地等待所有任务不同,尽管它们仍然异步运行。您应该在 WhenAll 情况下捕获 AggregateException。如果您必须创建一个 foreach 循环并且您打算在内部使用 await - 更好的控制是安排任务并使用 WhenAll。应涵盖此方案的主题是 TPL、TaskScheduler、PLINQ 和并行扩展。这完全是关于以一定的并行度异步运行一定数量或未知数量的任务。您的示例就像所有 url 的 ParallelForEach。

一些高级和通用的例子: https://stackoverflow.com/a/39174881/1025264

https://stackoverflow.com/a/57519218/1025264

TPL 变体: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.dataflow.actionblock-1.-ctor?redirectedfrom=MSDN&view=net-5.0#System_Threading_Tasks_Dataflow_ActionBlock_1__ctor_System_Action__0__System_Threading_Tasks_Dataflow_ExecutionDataflowBlockOptions_

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-08
    • 1970-01-01
    • 1970-01-01
    • 2016-08-22
    相关资源
    最近更新 更多