【问题标题】:How to await a list of tasks asynchronously using LINQ?如何使用 LINQ 异步等待任务列表?
【发布时间】:2014-03-19 01:14:12
【问题描述】:

我有一个这样创建的任务列表:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

通过使用.ToList(),任务应该全部启动。现在我想等待他们完成并返回结果。

这适用于上述... 块:

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

它可以满足我的要求,但这似乎很笨拙。我宁愿写一些像这样更简单的东西:

return tasks.Select(async task => await task).ToList();

... 但这不会编译。我错过了什么?还是不能这样表达?

【问题讨论】:

  • 您是否需要为每个 foo 串行处理 DoSomethingAsync(foo),或者这是 Parallel.ForEach<Foo> 的候选者?
  • @mdisibio - Parallel.ForEach 正在阻塞。这里的模式来自 Jon Skeet 的Asynchronous C# video on Pluralsight。它在没有阻塞的情况下并行执行。
  • @mdisibio - 不。它们并行运行。 Try it。 (此外,如果我只想使用WhenAll,我似乎不需要.ToList()。)
  • 点了。根据DoSomethingAsync 的编写方式,该列表可能会或可能不会并行执行。我能够编写一个测试方法和一个不是的版本,但在任何一种情况下,行为都是由方法本身决定的,而不是由创建任务的委托决定。很抱歉混淆了。但是,如果DoSomethingAsyc 返回Task&lt;Foo&gt;,那么委托中的await 并不是绝对必要的……我认为这是我要尝试提出的主要观点。
  • GetFoosAsync() 返回什么? @MattJohnson-品脱

标签: c# linq async-await


【解决方案1】:

Task.WhenAll 的一个问题是它会产生并行性。在大多数情况下,它可能会更好,但有时你想避免它。例如,从 DB 中批量读取数据并将数据发送到某个远程 Web 服务。您不想将所有批次加载到内存中,而是在处理前一个批次后访问数据库。所以,你必须打破异步性。这是一个例子:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

注意.GetAwaiter().GetResult() 将其转换为同步。只有在 batchSize of events 被处理后,DB 才会被延迟命中。

【讨论】:

    【解决方案2】:

    在斯蒂芬的answer上展开也可以不用.ToList()表示如下:

    var tasks = foos.Select(aFoo => aFoo.DoSomething());
    await Task.WhenAll(tasks).ConfigureAwait(true);
    

    背景:在某些情况下,调用.ToList() 可能会导致当时执行的副作用,因为随后枚举了可枚举。如果可枚举是对一组 API 或一组查询的调用,那么这可能不是当时所期望的行为。如果没有.ToList(),将在任务等待时枚举枚举。

    更具体地说:使用 (Fluent) NHibernate,您通常会避免在查询中使用.ToList(),否则您最终可能会读取整个结果集。这可能是您想要的更多数据。

    【讨论】:

    • 在这种情况下,ToList 并没有太大的不同。无论如何,Task.WhenAll 立即将提供的IEnumerable&lt;Task&lt;TResult&gt;&gt; 实现为一个数组 (source code)。
    • @TheodorZoulias 够公平的。但是,实施可能会发生变化。 - 通常,我避免使用.ToList(),因为它还取决于相关的 LINQ 提供程序。虽然目前,这是您提供的链接中的链接,但在您可能使用.ToList() 的所有场景中不一定都是如此。绝对值得了解 LINQ 提供程序的特定实现。因此,感谢您非常宝贵的评论,西奥多!
    【解决方案3】:

    为了扩展斯蒂芬的回答,我创建了以下扩展方法来保留 LINQ 的 fluent style。然后你可以做

    await someTasks.WhenAll()
    
    namespace System.Linq
    {
        public static class IEnumerableExtensions
        {
            public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
            {
                return Task.WhenAll(source);
            }
        }
    }
    

    【讨论】:

    • 就个人而言,我将您的扩展方法命名为ToArrayAsync
    【解决方案4】:

    LINQ 不适用于async 代码,但您可以这样做:

    var tasks = foos.Select(DoSomethingAsync).ToList();
    await Task.WhenAll(tasks);
    

    如果您的任务都返回相同类型的值,那么您甚至可以这样做:

    var results = await Task.WhenAll(tasks);
    

    这是相当不错的。 WhenAll返回一个数组,所以我相信你的方法可以直接返回结果:

    return await Task.WhenAll(tasks);
    

    【讨论】:

    • 只是想指出这也适用于var tasks = foos.Select(foo =&gt; DoSomethingAsync(foo)).ToList();
    • 甚至var tasks = foos.Select(DoSomethingAsync).ToList();
    • Linq 不能与异步代码完美配合的原因是什么?
    • @EhsanSajjad:因为 LINQ to Objects 同步处理内存中的对象。一些有限的事情有效,例如Select。但大多数都没有,比如Where
    • @EhsanSajjad:如果操作是基于I/O的,那么可以使用async来减少线程;如果它受 CPU 限制并且已经在后台线程上,那么 async 不会提供任何好处。
    【解决方案5】:

    Task.WhenAll 应该在这里解决问题。

    【讨论】:

      【解决方案6】:

      使用Task.WaitAllTask.WhenAll 中合适的那个。

      【讨论】:

      • 那也行不通。 Task.WaitAll 处于阻塞状态,不可等待,并且不能与 Task&lt;T&gt; 一起使用。
      • @MattJohnson WhenAll ?
      • 是的。而已!我觉得很笨。谢谢!
      猜你喜欢
      • 1970-01-01
      • 2014-03-18
      • 2014-09-15
      • 1970-01-01
      • 2014-09-06
      • 2013-02-10
      • 2017-07-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多