代码之家  ›  专栏  ›  技术社区  ›  John Reilly

任务.WhenAll选择是一把脚枪-但为什么?

  •  0
  • John Reilly  · 技术社区  · 6 年前

    考虑:您有一个用户id集合,希望从API中加载由其id表示的每个用户的详细信息。你想把所有这些用户打包成某种集合并发送回呼叫代码。你想用林肯。

    像这样的:

    var userTasks = userIds.Select(userId => GetUserDetailsAsync(userId));
    var users = await Task.WhenAll(tasks); // users is User[]
    

    这对我的应用程序很好,因为我的用户相对较少。但是,有一点,它没有规模。当它达到数千个用户的程度时,这会导致数千个HTTP请求同时被触发,并且 坏事开始发生

    不是一个值得骄傲的日子。

    Task.WhenAll Select 组合拳,我们可以摆脱这种模式。但我的问题是:

    这里出什么事了?

    当我读到关于这个主题的文章时,这个场景似乎被 Mark Heath's list of Async antipatterns :“过度并行化”:

    现在,这确实“管用”,但如果有10000个订单呢?我们已经向线程池中注入了数以千计的任务,可能会阻止其他有用的工作完成。如果ProcessOrderAsync对另一个服务(如数据库或微服务)进行下游调用,我们可能会用太多的调用来重载它。

    这就是原因吗?我问作为我的理解 async / await 我越是读到这个话题就越不清楚。从许多方面来看,很明显“线程不是任务”。这很酷,但是我的代码似乎耗尽了ASP.NET核心可以处理。

    任务.WhenAll 和 组合耗尽线程池或类似?或者还有其他我不知道的解释吗?

    更新:

    https://blog.johnnyreilly.com/2020/06/taskwhenall-select-is-footgun.html

    0 回复  |  直到 6 年前
        1
  •  3
  •   James Skimming    6 年前

    N+1问题

    你可能想找到一个不同的解决方案。您必须为所有用户执行此操作吗?如果是,那么可以切换到后台进程 fan-out

    回到步兵(顺便说一句,我得查一下)。

    在.NET Core中,它们通常在一个单独的线程上完成,如果不完成,则等待一个几乎肯定是这样的HTTP请求。

    您可能已经耗尽了线程池,但是由于您正在发出HTTP请求,我怀疑您已经耗尽了并发出站HTTP请求的数量。 “默认连接限制为ASP.NET托管应用程序和2个应用程序。” 参见文档 here

    有没有一种方法可以实现某种并行性而不消耗资源(线程或http连接)?-是的。

    下面是一个我经常实现的模式,因为这个原因,使用 Batch() 从 morelinq

    IEnumerable<User> users = Enumerable.Empty<User>();
    IEnumerable<IEnumerable<string>> batches = userIds.Batch(10);
    foreach (IEnumerable<string> batch in batches)
    {
        Task<User> batchTasks = batch.Select(userId => GetUserDetailsAsync(userId));
        User[] batchUsers = await Task.WhenAll(batchTasks);
        users = users.Concat(batchUsers);
    }
    

    仍然有10个异步HTTP请求 GetUserDetailsAsync() ,并且不会耗尽线程或并发的HTTP请求(或者至少最多10个)。

    在应用程序的其他地方大量使用,当系统负载不足时,您可能会遇到相同的限制,因此这种批处理并不总是一个好主意。YMMV公司。

        2
  •  1
  •   Guru Stron    6 年前

    当 there is no thread GetUserDetailsAsync 将等待一些IO绑定的操作继续(分析输出,返回结果…)将需要在某个线程上运行,以便 Task.Result 它是由 GetUserDetailsAsync

        3
  •  0
  •   Stephen Cleary    6 年前

    创建数千个任务没有问题。它们不是线。

    核心问题是你对API的冲击太大了。因此,最好的解决方案将改变您对API的调用方式:

    1. 使用 batch 如果API支持,则为其路由。
    2. 尽可能使用缓存。
    3. 最后,如果以上都不可能,请考虑限制API调用。

    SemaphoreSlim ,如下所示:

    using var throttler = new SemaphoreSlim(10);
    var userTasks = userIds.Select(async userId =>
    {
      await throttler.WaitAsync();
      try { await GetUserDetailsAsync(userId); }
      finally { throttler.Release(); }
    });
    var users = await Task.WhenAll(tasks); // users is User[]
    

    同样,只有当您不能首先进行设计更改以避免数千个API调用时,这种限制才是最好的。

    推荐文章