在這篇文章中,我們將研究如何異步使用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<TOut>> 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