【问题标题】:ForEach lambda async vs Task.WhenAllForEach lambda 异步与 Task.WhenAll
【发布时间】:2019-09-17 03:52:06
【问题描述】:

我有一个这样的异步方法:

private async Task SendAsync(string text) {
  ...
}

我还必须对 List 中的每个项目使用此方法一次:

List<string> textsToSend = new Service().GetMessages();

目前我的实现是这样的:

List<string> textsToSend = new Service().GetMessages();
List<Task> tasks = new List<Task>(textsToSend.Count);
textsToSend.ForEach(t => tasks.Add(SendAsync(t)));
await Task.WhenAll(tasks);

使用此代码,对于运行async 发送方法的每条消息,我都会得到一个Task

但是,我不知道我的实现和这个有什么不同:

List<string> textsToSend = new Service().GetMessages();
textsToSend.ForEach(async t => await SendAsync(t));

在第二个中,我没有List&lt;Task&gt; 分配,但我认为第一个并行启动所有Task,第二个样本一个一个。

您能否帮我澄清一下第一个和第二个样本之间是否有任何不同?

PD:我也知道 C#8 支持 foreach 异步,但我使用的是 C# 7

【问题讨论】:

  • 第二个无法正常工作,因为ForEach 无法与异步委托一起使用,因此它将被转换为Action,而这反过来又无法等待。

标签: c# .net asynchronous async-await


【解决方案1】:

您甚至不需要列表,更不用说 ForEach 来执行多个任务并等待所有任务。无论如何,ForEach 只是一个使用 `foreach 的便捷函数。

要基于输入列表并发执行一些异步调用,您只需要Enumerable.Select。要等待所有这些都完成,您只需要 Task.WhenAll

var tasks=textsToSend.Select(text=>SendAsync(text));
await Task.WhenAll(tasks);

LINQ 和 IEnumerable 通常使用惰性求值,这意味着 Select 的代码在返回的 IEnumerable 被迭代之前不会被执行。在这种情况下,这无关紧要,因为它在下一行进行了迭代。如果想强制所有任务开始调用ToArray() 就足够了,例如:

var tasks=textsToSend.Select(SendAsync).ToArray();

如果您想按顺序执行这些异步调用,即一个接一个,您可以使用简单的 foreach。不需要 C# 8 的 await foreach

foreach(var text in textsToSend)
{
    await SendAsync(text);
}

虫子

这一行只是一个错误:

textsToSend.ForEach(async t => await SendAsync(t));

ForEach 对任务一无所知,因此它从不等待生成的任务完成。事实上,任务根本无法等待。 async t 语法创建一个 async void 委托。相当于:

async void MyMethod(string t)
{
    await SendAsync(t);
}

textToSend.ForEach(t=>MyMethod(t));

这带来了async void方法的所有问题。由于应用程序对这些 async void 调用一无所知,因此它很容易在这些方法完成之前终止,从而导致 NRE、ObjectDisposedExceptions 和其他奇怪的问题。

参考大卫福勒的Implicit async void delegates

C# 8 和等待 foreach

C# 8 的 IAsyncEnumerable 在顺序情况下很有用,如果我们想在迭代器中返回每个异步操作的结果,一旦我们得到它们。

在 C# 8 之前,即使是顺序执行,也无法避免等待所有结果。我们必须将它们全部收集在一个列表中。假设每个操作都返回一个字符串,我们必须这样写:

async Task<List<string> SendTexts(IEnumerable<string> textsToSend)
{
    var results=new List<string>();
    foreach(var text in textsToSend)
    {
        var result=await SendAsync(text);
        results.Add(result);
    }
}

并将其与 :

一起使用
var results=await SendTexts(texts);

在 C# 8 中,我们可以返回单个结果并异步使用它们。我们也不需要在返回结果之前缓存结果:

async IAsyncEmumerable<string> SendTexts(IEnumerable<string> textsToSend)
{
    foreach(var text in textsToSend)
    {
        var result=await SendAsync(text);
        yield return;
    }
}


await foreach(var result in SendTexts(texts))
{
   ...
}

await foreach 只需要使用 IAsyncEnumerable 结果,而不是生成它

【讨论】:

  • 一个可能对具有少量 LINQ 经验的开发人员有用的注释(当然不是这个答案的作者!)。 var tasks = textsToSend.Select(text =&gt; SendAsync(text)) 行不创建任何任务。它创建了一个延迟的 LINQ 可枚举任务。这些任务是在调用Task.WhenAll(tasks) 时创建的。此方法在执行任何其他操作之前,通过将其转换为任务数组来实现作为参数接收的延迟枚举。
  • @TheodorZoulias 这就是我这样写的原因。不过,OP 的代码中还有更重要的问题,例如 async void 错误
  • 顺便说一句,我赞成你的回答。其他人是反对者。 :-)
  • 在最后一个 sn-p 中,是否还需要 var results=new List&lt;string&gt;();? (没有DV,也没有)
  • @Fildor 不,不是,但编辑一个长答案就像在观看 previous 页面时触摸打字 - 实际上,它不是“喜欢”,而是完全一样
【解决方案2】:

第一个并行启动所有任务

正确。并且await Task.WhenAll(tasks); 等待所有消息都发送完毕。

第二个也并行发送消息,但不等待所有消息都发送,因为您不等待任何任务。

在你的情况下:

textsToSend.ForEach(async t => await SendAsync(t));

等价于

textsToSend.ForEach(t => SendAsync(t));

async t =&gt; await SendAsync(t) 委托可以将任务(取决于可分配类型)返回为SendAsync(t)。如果将其传递给ForEachasync t =&gt; await SendAsync(t)SendAsync(t) 都将被转换为Action&lt;string&gt;

如果任何 SendAsync 抛出异常,第一个代码也会抛出异常。在第二个代码中,任何异常都将被忽略。

【讨论】:

  • ForEach(async t =&gt; await SendAsync(t)); 不返回任何任务,它是一个async void 委托。
  • 我写了async t =&gt; await SendAsync(t)返回任务。它是一个匿名方法,可以分配给Func&lt;string, Task&gt;,使用它和ForEach 方法中的SendAsync(t) 没有区别。他们都只是返回被忽略的任务。
  • 很遗憾没有。 List.ForEach 不返回 anything 并且只接受 Action&lt;T&gt;async t=&gt; 语法 can't 被翻译成async Task 不会导致编译错误,因此会生成async void。这是 David Fowler 解释的异步陷阱之一:implicit async void delegates
  • 我没有写到List.ForEach 会返回一些东西。好的。关于委托,添加了更准确的改进。
猜你喜欢
  • 1970-01-01
  • 2021-08-31
  • 1970-01-01
  • 2018-08-26
  • 2017-01-06
  • 2013-02-14
  • 1970-01-01
  • 2021-10-20
相关资源
最近更新 更多