ForEach lambda异步与Task.WhenAll

gan*_*o55 2 .net c# asynchronous async-await

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

private async Task SendAsync(string text) {
  ...
}
Run Code Online (Sandbox Code Playgroud)

对于列表中的每个项目,我还必须使用此方法一次:

List<string> textsToSend = new Service().GetMessages();
Run Code Online (Sandbox Code Playgroud)

目前,我的实现是这样的:

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);
Run Code Online (Sandbox Code Playgroud)

通过此代码,我Task为运行async发送方法的每条消息得到一个。

但是,我不知道我的实现与这个实现之间是否有任何区别:

List<string> textsToSend = new Service().GetMessages();
textsToSend.ForEach(async t => await SendAsync(t));
Run Code Online (Sandbox Code Playgroud)

在第二个示例中,我没有List<Task>分配,但是我认为第一个Task示例是并行启动的,第二个示例是一个接一个地启动的。

您能帮我澄清一下第一样本和第二样本之间是否有区别?

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

Pan*_*vos 5

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

要基于输入列表同时执行一些异步调用,您所需要做的就是Enumerable.Select。要等待他们全部完成,您只需要Task.WhenAll

var tasks=textsToSend.Select(text=>SendAsync(text));
await Task.WhenAll(tasks);
Run Code Online (Sandbox Code Playgroud)

LINQ和IEnumerable通常使用惰性求值,这意味着Select直到对返回的IEnumerable进行迭代之前,将不执行的代码。在这种情况下,这并不重要,因为它在下一行进行了迭代。如果要强制所有任务开始呼叫ToArray()就足够了,例如:

var tasks=textsToSend.Select(SendAsync).ToArray();
Run Code Online (Sandbox Code Playgroud)

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

foreach(var text in textsToSend)
{
    await SendAsync(text);
}
Run Code Online (Sandbox Code Playgroud)

错误

这行只是一个错误:

textsToSend.ForEach(async t => await SendAsync(t));
Run Code Online (Sandbox Code Playgroud)

ForEach对任务一无所知,因此它永远不会等待生成的任务完成。实际上,根本就不能等待任务。该async t语法创建一个async void委托。等效于:

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

textToSend.ForEach(t=>MyMethod(t));
Run Code Online (Sandbox Code Playgroud)

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

供参考,请参阅David Fowler的隐式异步void委托

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并与使用:

var results=await SendTexts(texts);
Run Code Online (Sandbox Code Playgroud)

在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))
{
   ...
}
Run Code Online (Sandbox Code Playgroud)

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

  • 对于具有少量 LINQ 经验的开发人员(当然不是这个答案的作者!)来说可能会有用。`vartasks = textsToSend.Select(text =&gt; SendAsync(text))` 行不会创建任何任务。它创建延迟的 LINQ 任务枚举。任务是在调用“Task.WhenAll(tasks)”时创建的。此方法在执行任何其他操作之前,通过将接收为参数的延迟枚举具体化为任务数组。 (2认同)