在这篇文章中,我们将研究如何异步使用foreach
循环进行迭代。现在你也许会想为什么我需要确定的知道如何去实现,我只要像这样做就好了...
//被调用的异步方法
public static Task DoAsync(string Item)
{
Task.Delay(1000);
Console.WriteLine($"Item: {Item}");
return Task.CompletedTask;
}
//循环方法
public static async Task BadLoopAsync(IEnumerable<string> thingsToLoop)
{
foreach (var thing in thingsToLoop)
{
await DoAsync(thing);
}
}
虽然这样同样可以运行,但并不是最好的实现方式。当我们在同步的循环中等待task一个接一个完成时,它太慢了。当然,如果每个task
都依赖于于上一个任务的完成且需要按照顺序完成,那很好。否则就浪费了。
Tasks and the promises they make(Task和Promises)
要理解为什么上面的代码不好,最好了解一下Tasks
及其工作方式。
深入解释Task
不在本文的讨论范围之内。因此,如果您想进一步深入研究,我会在下面提供一些链接。我还提供了一个与JavaScript
进行比较的链接,因为我认为它的异步开发实现是通过一种确实有助于理解它的方式完成的。
我尝试用一种基本概述来解释Task
是什么,简单来说,这是一个正在进行中的工作任务。通过异步方法返回的Task
实际上是在说:“Hey,这正在做一些工作,但是还没完成。所以这里有一个代表进行中工作的Task
。当这个工作完成了,我们将回到这里并继续”。
事实上,它承诺现在正在进行的工作一旦完成,它将回到这里继续运行。
Looping Asynchronously(异步循环)
对,我们终于可以解决本文要解决的问题了。当我们进行异步迭代的时候,有两件事需要考虑一下,那就是我们的方法是返回一个值还是无返回的。
Returning Void(无返回值)
首先,我们看一下无返回值时不一样的地方。
//Async method to be awaited
public static Task DoAsync(string Item)
{
Task.Delay(1000);
Console.WriteLine($"Item: {Item}");
return Task.CompletedTask;
}
//Method to iterate over collection and await DoAsync method
public static async Task LoopAsync(IEnumerable<string> thingsToLoop)
{
List<Task> listOfTasks = new List<Task>();
foreach (var thing in thingsToLoop)
{
listOfTasks.Add(DoAsync(thing));
}
await Task.WhenAll(listOfTasks);
}
上面的代码和文章开头的代码其实很相似。不同的地方在于使用await
关键字的方式不同。当一个方法被调用时,我们要做的第一件事就是创建一个Tasks集合(因为我们的方法返回一个Task
,异步说法,无返回值)。这里我们创建了一个 List<Task>
,当然也可以用其他的集合类型。一旦有了这个,我们就可以开始遍历被传入该方法的参数thingsToLoop
。
接下来就是我们的listOfTasks
集合起作用的地方了,比起之前直接在调用DoAsync
方法的地方等待它完成,我们直接将调用方法的Task返回值加入到集合中。现在,如果你还记得我们前面关于Tasks
的简短部分,这里就是承诺任务被完成的地方。
当我们将所有的Tasks加入到我们的列表中,我们就可以调用Task的静态方法WhenAll
。当你希望一次性等待一堆任务全部完成时,你可以使用这个方法。我们接下来await
该方法,等待集合中所有的的Tasks
完成。一旦所有任务都完成,该方法将已完成的任务返回给调用方,同时,我们的业务逻辑也以及完成了。
这解决了我们在第一个代码段中的问题。 我们不再在循环中一个接一个地等待每个任务的情况,我们等待所有任务全部完成,然后再将此方法的Task完成后返回给调用方。现在,应该继续进行流程中需要完成的过程。
Returning Values(有返回值)
现在,我们看一下在Task
完成时需要返回值时该怎么做。
//Async method to be awaited
public static Task<string> DoAsyncResult(string item)
{
Task.Delay(1000);
return Task.FromResult(item);
}
//Method to iterate over collection and await DoAsyncResult
public static async Task<IEnumerable<string>> LoopAsyncResult(IEnumerable<string> thingsToLoop)
{
List<Task<string>> listOfTasks = new List<Task<string>>();
foreach (var thing in thingsToLoop)
{
listOfTasks.Add(DoAsyncResult(thing));
}
return await Task.WhenAll<string>(listOfTasks);
}
你可以发现这段代码和无返回值时很相似。我们仍然创建一个Tasks
列表然后将调用异步方法DoAsyncResult
返回的Tasks
加入到集合中。
不同的地方在于返回类型和Task类型。相较于使用Task
(void的异步版本),我们返回Task<T>
(Task的通用实现)。Task<T>
允许指定Task完成后将返回的类型,在我们的示例中,这是一个string
。
我们的DoAsyncResult
返回一个字符串。因此当我们的集合中的Tasks完成时,它们也将会返回字符串。WhenAll
方法也有通用实现,可以设置返回类型。如您所见,我们的设置其为string
。WhenAll<T>
方法的返回类型是指定类型的IEnumerable
,我们可以轻松返回该类型。
C# 8 to the rescue
这个问题的解决方法已经在路上了。C# 8的下一个主流版本将包含Asynchronous Streams
,使用新特性,可以直接在foreach
循环上直接使用await
关键词。
await foreach (var name in GetNamesAsync())
上面的代码只是一个看起来类似的使用样例。我还没有尝试过这个或任何C# 8特性,但是它肯定添加了很多有趣的特性。如果你想了解更多内容可以点击这里。
Helpful Extensions (有用的拓展)
我提供了一些有用的扩展方法来实现上述功能。它们扩展了IEnumerable
接口,并且可以快速介入现有的需要使用异步处理集合项的情况。
public static Task LoopAsync<T>(this IEnumerable<T> list, Func<T, Task> function)
{
return Task.WhenAll(list.Select(function));
}
public async static Task<IEnumerable<S>> LoopAsyncResult<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, Task<TOut>> function)
{
var loopResult = await Task.WhenAll(list.Select(function));
return loopResult.ToList().AsEnumerable();
}
本文为个人翻译。原文地址: https://medium.com/@t.masonbarneydev/iterating-asynchronously-how-to-use-async-await-with-foreach-in-c-d7e6d21f89fa