异步等待在LINQ选择


178

我需要修改现有程序,它包含以下代码:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

但这对我来说似乎很奇怪,首先是在select中使用asyncawait。根据Stephen Cleary的回答,我应该可以删除那些内容。

然后第二个Select选择结果。这不是说任务根本不异步,而是同步执行(付出了很多努力却没有做),还是会异步执行任务,完成后是否执行其余查询?

我是否应该根据Stephen Cleary的另一个答案,像下面那样编写上面的代码:

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

这样完全一样吗?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

当我从事这个项目时,我想更改第一个代码示例,但是我不太想更改(似乎在工作)异步代码。也许我只是担心什么,而所有3个代码示例都做完全相同的事情?

ProcessEventsAsync看起来像这样:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}

ProceesEventAsync的返回类型是什么?
tede24 '16

@ tede24它Task<InputResult>InputResult属于定制类。
Alexander Derck '16

我认为您的版本更容易阅读。但是,您忘记Select了之前任务的结果 Where
Max

而且InputResult具有Result属性,对吗?
tede24 '16

@ tede24结果是任务的属性,而不是我的课程。@Max等待着应该确保我得到的结果不访问Result任务的属性
Alexander Derck

Answers:


184
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

但这对我来说似乎很奇怪,首先在select中使用async和await。根据Stephen Cleary的回答,我应该可以删除那些内容。

的呼叫Select有效。这两行基本上是相同的:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(关于如何从中引发同步异常,有一些细微的差别ProcessEventAsync,但是在此代码的上下文中,根本没有关系。)

然后第二个Select选择结果。这不是说任务根本就不异步,而是同步执行(不费吹灰之力就完成了),还是将任务异步执行,并在完成其余查询后执行?

这意味着查询正在阻塞。因此它并不是真正的异步。

分解:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

将首先为每个事件启动一个异步操作。然后这行:

                   .Select(t => t.Result)

会一次等待这些操作完成(首先等待第一个事件的操作,然后等待下一个,然后再等待下一个,等等)。

这是我不关心的部分,因为它会阻塞并且还会在中包装任何异常AggregateException

这样完全一样吗?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

是的,这两个例子是等效的。它们都开始所有异步操作(events.Select(...)),然后异步等待所有操作以任何顺序(await Task.WhenAll(...))完成,然后继续其余工作(Where...)。

这两个示例均与原始代码不同。原始代码受阻,并将包裹异常AggregateException


为清除而欢呼!因此,不是将异常包装成一个包,而是AggregateException在第二个代码中获得多个单独的异常?
Alexander Derck '16

1
@AlexanderDerck:不,在新旧代码中,只会引发第一个异常。但随着Result它会被包裹AggregateException
Stephen Cleary

使用此代码,我的ASP.NET MVC控制器陷入僵局。我使用Task.Run(…)解决了它。我对此感觉不太好。但是,在运行异步xUnit测试时,它恰好完成了。这是怎么回事?
SuperJMN

2
@SuperJMN:替换stuff.Select(x => x.Result);await Task.WhenAll(stuff)
Stephen Cleary,

1
@DanielS:它们本质上是相同的。存在一些差异,例如状态机,捕获上下文,同步异常的行为。更多信息,请访问blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary,

25

现有代码正在运行,但是阻塞了线程。

.Select(async ev => await ProcessEventAsync(ev))

为每个事件创建一个新任务,但是

.Select(t => t.Result)

阻止线程等待每个新任务结束。

另一方面,您的代码产生相同的结果,但保持异步。

仅对您的第一个代码发表评论。这条线

var tasks = await Task.WhenAll(events...

将产生一个Task,因此变量应以单数命名。

最后,您的最后一个代码相同,但更简洁

供参考:Task.Wait / Task.WhenAll


那么第一个代码块实际上是同步执行的吗?
Alexander Derck '16

1
是的,因为访问Result会生成一个Wait,它将阻塞线程。另一方面,当产生新任务时您可以等待。
tede24 '16

1
回到这个问题,看看您对tasks变量名称的评论,您是完全正确的。可怕的选择,因为他们马上就等待着,所以它们甚至都不是任务。我只是将问题留在原处
Alexander Derck

13

在Linq中可用的当前方法看来非常难看:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

希望.NET之后的版本将提供更优雅的工具来处理任务集合和集合任务。


12

我使用以下代码:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

像这样:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

5
这只是以一种更加晦涩的方式包装了现有功能imo
Alexander Derck,

替代方法是var result = await Task.WhenAll(sourceEnumerable.Select(async s => await someFunction(s,other params)))。它也可以工作,但不是LINQy
Siderite Zackwehdex

不应在第二部分代码中Func<TSource, Task<TResult>> method包含other params提到的内容?
matramos

2
额外的参数是外部的,具体取决于我要执行的功能,它们与扩展方法的上下文无关。
Siderite Zackwehdex

4
那是一个可爱的扩展方法。不知道为什么将其视为“更加晦涩”-语义上类似于sync Select(),因此是一个优雅的插件。
nullPainter

10

我更喜欢将此作为扩展方法:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

因此它可用于方法链接:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()

1
Wait当它没有真正等待时,您不应调用该方法。它创建的任务在所有任务完成时就完成了。调用它WhenAll,就像Task它模拟的方法一样。该方法也没有意义async。只需致电WhenAll并完成即可。
–Servy

在我看来,当它调用原始方法时,有点无用的包装器
Alexander Derck

@Servy公平点,但我并不特别喜欢任何名称选项。WhenAll听起来像是一个不完全的事件。
达里尔

3
@AlexanderDerck的优点是您可以在方法链中使用它。
达里尔

1
@Daryl因为WhenAll返回一个评估列表(它没有被延迟地评估),所以可以使用Task<T[]>返回类型来表示一个参数。等待时,它仍然可以使用Linq,但同时也表明它不是惰性的。
JAD
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.