[英]Nesting await in Parallel.ForEach
在 Metro 應用程序中,我需要執行多個 WCF 調用。 需要進行大量調用,因此我需要在並行循環中執行它們。 問題是並行循環在 WCF 調用全部完成之前退出。
您將如何重構它以按預期工作?
var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customers = new System.Collections.Concurrent.BlockingCollection<Customer>();
Parallel.ForEach(ids, async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
});
foreach ( var customer in customers )
{
Console.WriteLine(customer.ID);
}
Console.ReadKey();
Parallel.ForEach()
背后的整個想法是你有一組線程,每個線程處理集合的一部分。 正如您所注意到的,這不適用於async
- await
,您希望在異步調用期間釋放線程。
你可以通過阻塞ForEach()
線程來“修復”這個問題,但這會破壞async
- await
的全部意義。
您可以做的是使用TPL Dataflow而不是Parallel.ForEach()
,后者很好地支持異步Task
。
具體來說,您的代碼可以使用TransformBlock
編寫,該TransformBlock
使用async
lambda 將每個 id 轉換為Customer
。 該塊可以配置為並行執行。 您可以將該塊鏈接到將每個Customer
寫入控制台的ActionBlock
。 設置塊網絡后,您可以Post()
每個 id 到TransformBlock
。
在代碼中:
var ids = new List<string> { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var getCustomerBlock = new TransformBlock<string, Customer>(
async i =>
{
ICustomerRepo repo = new CustomerRepo();
return await repo.GetCustomer(i);
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
});
var writeCustomerBlock = new ActionBlock<Customer>(c => Console.WriteLine(c.ID));
getCustomerBlock.LinkTo(
writeCustomerBlock, new DataflowLinkOptions
{
PropagateCompletion = true
});
foreach (var id in ids)
getCustomerBlock.Post(id);
getCustomerBlock.Complete();
writeCustomerBlock.Completion.Wait();
盡管您可能希望將TransformBlock
的並行性限制為某個小常量。 此外,您可以限制TransformBlock
的容量並使用SendAsync()
向其異步添加項目,例如,如果集合太大。
與您的代碼(如果它有效)相比,一個額外的好處是寫入將在單個項目完成后立即開始,而不是等到所有處理完成。
svick 的回答(像往常一樣)非常好。
但是,我發現當您實際上有大量數據要傳輸時,Dataflow 會更有用。 或者當你需要一個async
兼容的隊列時。
在您的情況下,一個更簡單的解決方案是只使用async
樣式的並行性:
var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customerTasks = ids.Select(i =>
{
ICustomerRepo repo = new CustomerRepo();
return repo.GetCustomer(i);
});
var customers = await Task.WhenAll(customerTasks);
foreach (var customer in customers)
{
Console.WriteLine(customer.ID);
}
Console.ReadKey();
按照 svick 的建議使用 DataFlow 可能有點矯枉過正,Stephen 的回答沒有提供控制操作並發性的方法。 但是,這可以很簡單地實現:
public static async Task RunWithMaxDegreeOfConcurrency<T>(
int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
var activeTasks = new List<Task>(maxDegreeOfConcurrency);
foreach (var task in collection.Select(taskFactory))
{
activeTasks.Add(task);
if (activeTasks.Count == maxDegreeOfConcurrency)
{
await Task.WhenAny(activeTasks.ToArray());
//observe exceptions here
activeTasks.RemoveAll(t => t.IsCompleted);
}
}
await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t =>
{
//observe exceptions in a manner consistent with the above
});
}
ToArray()
調用可以通過使用數組而不是列表並替換已完成的任務來優化,但我懷疑它在大多數情況下會產生很大的不同。 每個 OP 問題的示例用法:
RunWithMaxDegreeOfConcurrency(10, ids, async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
});
EDIT Fellow SO 用戶和 TPL 奇才Eli Arbel向我指出了Stephen Toub 的一篇相關文章。 像往常一樣,他的實現既優雅又高效:
public static Task ForEachAsync<T>(
this IEnumerable<T> source, int dop, Func<T, Task> body)
{
return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(dop)
select Task.Run(async delegate {
using (partition)
while (partition.MoveNext())
await body(partition.Current).ContinueWith(t =>
{
//observe exceptions
});
}));
}
您可以使用新的AsyncEnumerator NuGet Package節省工作量,該包在 4 年前最初發布問題時還不存在。 它允許您控制並行度:
using System.Collections.Async;
...
await ids.ParallelForEachAsync(async i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = await repo.GetCustomer(i);
customers.Add(cust);
},
maxDegreeOfParallelism: 10);
免責聲明:我是 AsyncEnumerator 庫的作者,該庫是開源的並在 MIT 許可下發布,我發布此消息只是為了幫助社區。
將Parallel.Foreach
包裝到Task.Run()
,而不是使用await
關鍵字使用[yourasyncmethod].Result
(您需要執行 Task.Run 以不阻塞 UI 線程)
像這樣的東西:
var yourForeachTask = Task.Run(() =>
{
Parallel.ForEach(ids, i =>
{
ICustomerRepo repo = new CustomerRepo();
var cust = repo.GetCustomer(i).Result;
customers.Add(cust);
});
});
await yourForeachTask;
這應該非常有效,而且比讓整個 TPL 數據流工作更容易:
var customers = await ids.SelectAsync(async i =>
{
ICustomerRepo repo = new CustomerRepo();
return await repo.GetCustomer(i);
});
...
public static async Task<IList<TResult>> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector, int maxDegreesOfParallelism = 4)
{
var results = new List<TResult>();
var activeTasks = new HashSet<Task<TResult>>();
foreach (var item in source)
{
activeTasks.Add(selector(item));
if (activeTasks.Count >= maxDegreesOfParallelism)
{
var completed = await Task.WhenAny(activeTasks);
activeTasks.Remove(completed);
results.Add(completed.Result);
}
}
results.AddRange(await Task.WhenAll(activeTasks));
return results;
}
一種使用 SemaphoreSlim 並允許設置最大並行度的擴展方法
/// <summary>
/// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">Type of IEnumerable</typeparam>
/// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
/// <param name="action">an async <see cref="Action" /> to execute</param>
/// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
/// Must be grater than 0</param>
/// <returns>A Task representing an async operation</returns>
/// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
public static async Task ForEachAsyncConcurrent<T>(
this IEnumerable<T> enumerable,
Func<T, Task> action,
int? maxDegreeOfParallelism = null)
{
if (maxDegreeOfParallelism.HasValue)
{
using (var semaphoreSlim = new SemaphoreSlim(
maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
{
var tasksWithThrottler = new List<Task>();
foreach (var item in enumerable)
{
// Increment the number of currently running tasks and wait if they are more than limit.
await semaphoreSlim.WaitAsync();
tasksWithThrottler.Add(Task.Run(async () =>
{
await action(item).ContinueWith(res =>
{
// action is completed, so decrement the number of currently running tasks
semaphoreSlim.Release();
});
}));
}
// Wait for all tasks to complete.
await Task.WhenAll(tasksWithThrottler.ToArray());
}
}
else
{
await Task.WhenAll(enumerable.Select(item => action(item)));
}
}
示例用法:
await enumerable.ForEachAsyncConcurrent(
async item =>
{
await SomeAsyncMethod(item);
},
5);
我參加聚會有點晚了,但您可能想考慮使用 GetAwaiter.GetResult() 在同步上下文中運行您的異步代碼,但如下所示;
Parallel.ForEach(ids, i =>
{
ICustomerRepo repo = new CustomerRepo();
// Run this in thread which Parallel library occupied.
var cust = repo.GetCustomer(i).GetAwaiter().GetResult();
customers.Add(cust);
});
在介紹了一堆輔助方法之后,您將能夠使用以下簡單的語法運行並行查詢:
const int DegreeOfParallelism = 10;
IEnumerable<double> result = await Enumerable.Range(0, 1000000)
.Split(DegreeOfParallelism)
.SelectManyAsync(async i => await CalculateAsync(i).ConfigureAwait(false))
.ConfigureAwait(false);
這里發生的事情是:我們將源集合分成 10 個塊( .Split(DegreeOfParallelism)
),然后運行 10 個任務,每個任務一個一個地處理其項目( .SelectManyAsync(...)
)並將它們合並回一個列表。
值得一提的是,有一種更簡單的方法:
double[] result2 = await Enumerable.Range(0, 1000000)
.Select(async i => await CalculateAsync(i).ConfigureAwait(false))
.WhenAll()
.ConfigureAwait(false);
但它需要一個預防措施:如果你有一個太大的源集合,它會立即為每個項目安排一個Task
,這可能會導致顯着的性能下降。
上述示例中使用的擴展方法如下所示:
public static class CollectionExtensions
{
/// <summary>
/// Splits collection into number of collections of nearly equal size.
/// </summary>
public static IEnumerable<List<T>> Split<T>(this IEnumerable<T> src, int slicesCount)
{
if (slicesCount <= 0) throw new ArgumentOutOfRangeException(nameof(slicesCount));
List<T> source = src.ToList();
var sourceIndex = 0;
for (var targetIndex = 0; targetIndex < slicesCount; targetIndex++)
{
var list = new List<T>();
int itemsLeft = source.Count - targetIndex;
while (slicesCount * list.Count < itemsLeft)
{
list.Add(source[sourceIndex++]);
}
yield return list;
}
}
/// <summary>
/// Takes collection of collections, projects those in parallel and merges results.
/// </summary>
public static async Task<IEnumerable<TResult>> SelectManyAsync<T, TResult>(
this IEnumerable<IEnumerable<T>> source,
Func<T, Task<TResult>> func)
{
List<TResult>[] slices = await source
.Select(async slice => await slice.SelectListAsync(func).ConfigureAwait(false))
.WhenAll()
.ConfigureAwait(false);
return slices.SelectMany(s => s);
}
/// <summary>Runs selector and awaits results.</summary>
public static async Task<List<TResult>> SelectListAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector)
{
List<TResult> result = new List<TResult>();
foreach (TSource source1 in source)
{
TResult result1 = await selector(source1).ConfigureAwait(false);
result.Add(result1);
}
return result;
}
/// <summary>Wraps tasks with Task.WhenAll.</summary>
public static Task<TResult[]> WhenAll<TResult>(this IEnumerable<Task<TResult>> source)
{
return Task.WhenAll<TResult>(source);
}
}
下面是ForEachAsync
方法的簡單通用實現,它基於來自TPL 數據流庫的ActionBlock
,現在嵌入到 .NET 5 平台中:
public static Task ForEachAsync<T>(this IEnumerable<T> source,
Func<T, Task> action, int dop)
{
// Arguments validation omitted
var block = new ActionBlock<T>(action,
new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = dop });
try
{
foreach (var item in source) block.Post(item);
block.Complete();
}
catch (Exception ex) { ((IDataflowBlock)block).Fault(ex); }
return block.Completion;
}
此解決方案急切地枚舉提供的IEnumerable
,並立即將其所有元素發送到ActionBlock
。 所以它不太適合具有大量元素的可枚舉。 下面是一種更復雜的方法,它懶惰地枚舉源,並將其元素一個一個地發送到ActionBlock
:
public static async Task ForEachAsync<T>(this IEnumerable<T> source,
Func<T, Task> action, int dop)
{
// Arguments validation omitted
var block = new ActionBlock<T>(action, new ExecutionDataflowBlockOptions()
{ MaxDegreeOfParallelism = dop, BoundedCapacity = dop });
try
{
foreach (var item in source)
if (!await block.SendAsync(item).ConfigureAwait(false)) break;
block.Complete();
}
catch (Exception ex) { ((IDataflowBlock)block).Fault(ex); }
try { await block.Completion.ConfigureAwait(false); }
catch { block.Completion.Wait(); } // Propagate AggregateException
}
這兩種方法在異常情況下具有不同的行為。 第一個¹傳播一個AggregateException
直接在其InnerExceptions
屬性中包含InnerExceptions
。 第二個傳播一個AggregateException
,其中包含另一個帶有異常的AggregateException
。 我個人發現第二種方法的行為在實踐中更方便,因為等待它會自動消除一層嵌套,所以我可以簡單地catch (AggregateException aex)
並處理catch
塊內的aex.InnerExceptions
。 第一種方法需要在等待Task
之前存儲它,以便我可以訪問catch
塊內的task.Exception.InnerExceptions
。 有關從異步方法傳播異常的更多信息,請查看此處或此處。
兩種實現都可以優雅地處理在source
枚舉期間可能發生的任何錯誤。 ForEachAsync
方法在所有掛起操作完成之前不會完成。 沒有任何任務被遺忘(以即發即忘的方式)。
¹第一個實現省略了 async 和 await 。
沒有 TPL 的簡單原生方式:
int totalThreads = 0; int maxThreads = 3;
foreach (var item in YouList)
{
while (totalThreads >= maxThreads) await Task.Delay(500);
Interlocked.Increment(ref totalThreads);
MyAsyncTask(item).ContinueWith((res) => Interlocked.Decrement(ref totalThreads));
}
您可以通過下一個任務檢查此解決方案:
async static Task MyAsyncTask(string item)
{
await Task.Delay(2500);
Console.WriteLine(item);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.