在Parallel.ForEach中嵌套等待


183

在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();

Answers:


171

背后的整个想法Parallel.ForEach()是,您有一组线程,每个线程处理集合的一部分。正如您所注意到的,这不适用于async- await,您要在异步调用期间释放线程。

您可以通过阻塞ForEach()线程来“修复”该问题,但这使async- 的全部观点无效await

您可以做的是使用TPL Dataflow代替Parallel.ForEach(),它Task很好地支持异步。

具体来说,您的代码可以使用编写TransformBlock,该代码Customer使用asynclambda 将每个id转换为。可以将该块配置为并行执行。您可以将该块链接到ActionBlock写入每个Customer控制台。设置区块网络后,您可以将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(),例如,如果集合太大。

与您的代码(如果可行)相比,另一个好处是,写入将在单个项目完成后立即开始,而不必等到所有处理都完成了。


2
异步,响应式扩展,TPL和TPL DataFlow的非常简短的概述-vantsuyoshi.wordpress.com/2012/01/05/…对于那些像我这样需要澄清的人。
诺曼H

1
我很确定这个答案不会并行处理。我相信您需要对id进行Parallel.ForEach并将其发布到getCustomerBlock。至少这是我测试此建议时发现的。
JasonLind

4
@JasonLind确实如此。使用Parallel.ForEach()Post()并行的项目应该不会有什么实际效果。
svick

1
@svick好吧,我找到了它,ActionBlock也需要处于并行状态。我的操作稍有不同,我不需要转换,因此只使用了缓冲块,并在ActionBlock中完成了工作。我对互联网上的另一个答案感到困惑。
JasonLind

2
我的意思是像在示例中在TransformBlock上一样,在ActionBlock上指定MaxDegreeOfParallelism
JasonLind

125

svick的答案(与往常一样)非常好。

但是,当您实际上有大量数据要传输时,我发现Dataflow更加有用。或者,当您需要async兼容的队列时。

在您的情况下,一个更简单的解决方案是仅使用async-style并行性:

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();

13
如果您想手动限制并行性(在这种情况下很可能这样做),则以这种方式进行操作会更加复杂。
svick

1
但是您说对了,Dataflow可能会非常复杂(例如,与比较时Parallel.ForEach())。但我认为,这是目前几乎async可以对收藏进行任何处理的最佳选择。
svick

1
@JamesManning将如何ParallelOptions提供帮助?它仅适用于Parallel.For/ForEach/Invoke,因为建立的OP在这里没有用。
Ohad Schneider

1
@StephenCleary如果GetCustomer方法返回a Task<T>,应该使用Select(async i => { await repo.GetCustomer(i);});吗?
Shyju

5
@batmaci:Parallel.ForEach不支持async
史蒂芬·克雷里

79

如建议的那样使用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);
});

编辑研究员SO用户和TPL wiz 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
                          });

        })); 
}

1
@RichardPierre实际上是这种过载Partitioner.Create使用块分区,它动态地为不同的任务提供元素,因此不会发生您描述的情况。还要注意,由于开销较小(特别是同步),在某些情况下静态(预定)分区可能会更快。有关更多信息,请参见:msdn.microsoft.com/en-us/library/dd997411(v=vs.110).aspx
Ohad Schneider

1
@OhadSchneider在//观察异常,如果抛出异常,它将冒泡给调用者吗?例如,如果我想让整个枚举数的任何部分失败就停止处理/失败?
特里

3
@Terry会在某种意义上冒泡给调用者,因为最高的任务(由创建Task.WhenAll)将包含异常(在内AggregateException),因此,如果所说的调用者使用await,则会在调用站点中引发异常。但是,Task.WhenAll仍将等待所有任务完成,并GetPartitionspartition.MoveNext调用时动态分配元素,直到没有更多元素要处理为止。这意味着,除非您添加自己的机制来停止处理(例如CancellationToken),否则它不会自行发生。
奥哈德·施耐德

1
@gibbocool我仍然不确定我是否遵循。假设您总共有7个任务,并在注释中指定了参数。进一步假设第一批次偶尔执行5秒任务和三个1秒任务。大约一秒钟后,5秒任务仍将执行,而三个1秒任务将完成。此时,剩下的三个1秒任务将开始执行(它们将由分区程序提供给三个“空闲”线程)。
Ohad Schneider

1
@MichaelFreidgeim,您可以执行类似var current = partition.Current之前的操作await body,然后current在延续(ContinueWith(t => { ... })中使用。
Ohad Schneider

41

您可以使用新的AsyncEnumerator NuGet包节省精力,该在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许可的开放源代码,并且我发布此消息只是为了帮助社区。


11
谢尔盖,你应披露你是库的作者
迈克尔Freidgeim

5
好的,添加了免责声明。我没有从广告中谋取任何利益,只是想帮助人们;)
Serge Semenov

您的库与.NET Core不兼容。
Corniel Nobel

2
@CornielNobel,它与.NET Core兼容-GitHub上的源代码涵盖了.NET Framework和.NET Core的测试范围。
Serge Semenov

1
@SergeSemenov我已经对其库进行了很多使用AsyncStreams,我不得不说它很棒。无法充分推荐此库。
WBuck

16

Parallel.Foreach换成a Task.Run(),而不是await关键字use[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;

3
这是什么问题?我完全是这样做的。让我们Parallel.ForEach进行并行工作,直到所有工作都完成为止,然后将其阻塞,然后将整个过程推送到后台线程以具有响应式UI。有什么问题吗?也许这是一个太多的睡眠线程,但这是简短易读的代码。
ygoe 2015年

@LonelyPixel我唯一的问题是它最好Task.Run在when 调用TaskCompletionSource
Gusdor '16

1
@Gusdor好奇-为什么更TaskCompletionSource可取?
海鱼

@Seafish我希望我能回答一个好问题。一定是艰难的一天:D
古斯多

只是一个简短的更新。我现在一直在寻找这个,向下滚动以找到最简单的解决方案,然后再次找到自己的评论。我完全使用了此代码,它可以按预期工作。它仅假定循环中存在原始Async调用的Sync版本。await可以在前面移动以保存额外的变量名称。
ygoe '17

7

与使整个TPL Dataflow正常工作相比,这应该是非常有效的,并且更容易:

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;
}

不应该使用实例的使用await,如:var customers = await ids.SelectAsync(async i => { ... });
Paccc 2014年

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);
});

5

一个扩展方法,它使用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);

5

引入了许多辅助方法之后,您将可以使用以下简单语法运行并行查询:

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);
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.