【问题标题】:Nested Async/Await Doesn't Appear To Be Scaling嵌套的 Async/Await 似乎没有扩展
【发布时间】:2015-08-18 13:41:04
【问题描述】:

我有以下(简化的)代码:

public async Task GetData(DomainObject domainObject, int depth)
{
  // This async operation is really quick, and there's usually like five.
  IEnumerable<TierOne> tierOnes = await domainObject.GetTierOnesAsync();

  var tierOneTasks = tierOnes.Select(async tierOne => 
  {
    // This async operation is really quick and there's usually like three.
    IEnumerable<TierTwo> tierTwos = await tierOne.GetTierTwosAsync();

    if (depth <= TierTwoDepth)
      return;

    var tierTwoTasks = tierTwos.Select(async tierTwo => 
    {
      // This async operation is usually fast, and there's usually >= 100.
      IEnumerable<TierThree> tierThrees = await tierTwo.GetTierThreesAsync();

      if (depth <= TierThreeDepth)
        return;

      var tierThreeTasks = tierThrees.Select(async tierThree => 
      {
        // This async operation is SLOW, and there's usually.. 50?
        await tierThree.GetTierFoursAsync();
      });

      await Task.WhenAll(tierThreeTasks.ToArray());
    });

    await Task.WhenAll(tierTwoTasks.ToArray());
  });

  await Task.WhenAll(tierOneTasks.ToArray());
}

根据我所看到的,它的扩展性似乎不是很好。所有Async 操作都是“真正的异步”操作,这意味着它们都是 I/O。

我是否在这种情况下错误地使用了 Async/Await?根据我目前的观察,它没有达到我的预期。 TPL DataFlow 会是我的解决方案吗?

【问题讨论】:

  • “它似乎扩展得很好”是一个错字,你想把not放在那里吗?如果是这样,以何种方式扩展,您希望它完成得更快还是不会给系统带来那么多负载?您如何测试缩放比例?
  • 您使用了很多 IEnumerables 作为异步返回值。您确定延迟执行不会干扰您假设的并行化吗?
  • 异步并没有让它更快,事实上,由于开销,它通常比相同代码的同步版本慢一点,它为你做的是在你开始之前允许更高的负载以获得性能下降。
  • @Cameron:由于您的方法都在执行 I/O(可能针对同一台服务器),请仔细检查您的 ServicePointManager.DefaultConnectionLimit 设置。或者只是在启动时将其设置为int.MaxValue,看看问题是否仍然存在。
  • 客户端(浏览器、应用程序等)不应该向同一个域发出超过两个 HTTP 请求的非官方标准缺乏。一个更好的实现方式是使用具有例如 10 个并发任务的 ActionBlock,这样您就可以控制并发请求的数量,即使您有 100 个 URL 可以访问。更好的是,您可以在每层有一个具有不同 DOP 设置的块,将它们的结果提供给下一层

标签: c# asynchronous async-await tpl-dataflow


【解决方案1】:

对于对 GetData 的单个调用,嵌套的 async/await 调用不会引入任何并发。您检索所有 tierOne,然后检索 tierOne-#1 的所有 tierTwos,然后检索 tierTwo-#1 的所有 tierThrees,依此类推,所有这些都按顺序运行(尽管 GetTier*Async 方法中可能存在一些并发)。

如果你想要并发请求,那么 TPL Dataflow 确实是一个更好的解决方案。

【讨论】:

  • 我认为情况并非如此。一个级别中的所有任务都是(正确地)独立启动的(因为 Select 方法,以及稍后由 WhenAll 方法“等待”)。并发不是由 async/await 引入的,而是 LINQ 用于创建任务的事实。我唯一能想到的是他只是用完了线程,它发生在你处理嵌套异步操作的情况下(它几乎就像一个递归,显然在几个级别后停止 - 但不是在耗尽线程池之前)。
  • @Miklós 正如 OP 所写,他的 GetAsync 方法是真正的异步 I/O,因此它们在运行时不会阻塞任何线程。我从DispatcherSynchronizationContext 运行GetData(带有Task.Delay 调用),并且没有并发,所以线程池不能限制性能。从空同步上下文运行GetData 确实会引入并发性,但是如果到服务器的 I/O 比发送/接收代码慢(如 OP 所述),线程池中的瓶颈只是总数的一小部分等待时间。他的瓶颈可能在其他地方(就像 Stephen Cleary 建议的那样)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-21
  • 1970-01-01
  • 1970-01-01
  • 2018-07-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多