如何使用LINQ异步等待任务列表?


87

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

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

...但是不能编译。我想念什么?还是不可能以此方式表达事物?


您是否需要DoSomethingAsync(foo)为每个foo顺序处理,或者这是Parallel.ForEach <Foo>的候选对象?
mdisibio 2014年

1
@mdisibio-Parallel.ForEach正在阻止。这里的模式来自Jon Skeet的Pluralsight上Asynchronous C#视频。它并行执行而不会阻塞。
Matt Johnson-Pint

@mdisibio-不 它们并行运行。 试试吧。(此外,.ToList()如果我只是想使用它,则看起来不需要WhenAll。)
Matt Johnson-Pint 2014年

点了。根据如何DoSomethingAsync编写,列表可能会并行执行,也可能不会并行执行。我能够编写一个测试方法,一个版本不是,但无论哪种情况,行为都是由方法本身而不是由创建任务的委托决定的。对不起,我很困惑。但是,如果DoSomethingAsycreturn Task<Foo>,则await委托中的in并不是绝对必要的...我认为这是我要尝试提出的重点。
mdisibio 2014年

Answers:


136

LINQ不能与async代码完美配合,但是您可以这样做:

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

如果您的任务都返回相同类型的值,那么您甚至可以执行以下操作:

var results = await Task.WhenAll(tasks);

很好 WhenAll返回一个数组,所以我相信您的方法可以直接返回结果:

return await Task.WhenAll(tasks);

11
只是想指出,这也可以用于var tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio 2014年

1
甚至是var tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier 2014年

3
Linq与异步代码无法完美配合的背后原因是什么?
Ehsan Sajjad

2
@EhsanSajjad:因为LINQ to Objects在内存中对象上同步工作。某些功能有限,例如Select。但是大多数都不喜欢Where
史蒂芬·克利西

4
@EhsanSajjad:如果该操作基于I / O,则可以使用它async来减少线程;如果它已受CPU限制并且已经在后台线程上,则async不会提供任何好处。
史蒂芬·克利西

9

为了扩展斯蒂芬的答案,我创建了以下扩展方法来保持LINQ的流畅风格。然后你可以做

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

10
就我个人而言,我将命名您的扩展方法ToArrayAsync
torvin 2015年

3

Task.WhenAll的一个问题是它将创建并行性。在大多数情况下,它可能会更好,但是有时您想要避免这种情况。例如,从数据库中批量读取数据并将数据发送到某个远程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事件后,才可以偷懒地打数据库。



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.