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

回答:


172

背後にParallel.ForEach()ある全体的なアイデアは、一連のスレッドがあり、各スレッドがコレクションの一部を処理するということです。お気づきのとおり、これはasync- awaitで機能しません。非同期呼び出しの間、スレッドを解放する必要があります。

ForEach()スレッドをブロックすることでこれを「修正」することはできますが、それはasync-の全体的なポイントを無効にしawaitます。

あなたができることは、非同期のsをうまくサポートするの代わりにTPL Dataflowを使用することです。Parallel.ForEach()Task

具体的には、あなたのコードを使って書くこともできますTransformBlockに各IDを変換しているCustomer使用してasyncラムダを。このブロックは、並列実行するように構成できます。そのブロックを、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()ても、実際の効果はありません。
2015

1
@svickわかりました。ActionBlockも並列である必要があります。私は少し異なってそれをしていました、私は変換を必要としなかったので、私はただバッファブロックを使用し、ActionBlockで私の仕事をしました。私はインターウェブで別の答えに戸惑いました。
JasonLind

2
つまり、例のTransformBlockで行うように、ActionBlockでMaxDegreeOfParallelismを指定するということです
JasonLind

125

スヴィックの答えは(いつものように)素晴らしいです。

ただし、実際に大量のデータを転送する場合は、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();

13
手動で並列処理を制限する場合(この場合は可能性が高いです)、この方法で行うと、より複雑になります。
12

1
しかし、Dataflowはかなり複雑になる可能性があることは間違いありません(たとえばと比較するとParallel.ForEach())。しかし、私は現在、asyncコレクションでほとんどすべての作業を行うための最良のオプションだと思います。
12

1
@JamesManningはどのようParallelOptionsに役立つでしょうか?これはにのみ適用さParallel.For/ForEach/Invokeれ、OPが確立されたため、ここでは使用されません。
Ohad Schneider 2014

1
@StephenCleary GetCustomerメソッドがを返す場合、Task<T>使用する必要がありSelect(async i => { await repo.GetCustomer(i);});ますか?
Shyju 2016年

5
@batmaci:Parallel.ForEachはサポートしていませんasync
Stephen Cleary

81

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フェローSOユーザーとTPLウィズイーライアーベル、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 2016

1
@OhadSchneider //例外を観察し、例外がスローされた場合、呼び出し元にバブルアップしますか?たとえば、列挙型全体の一部が失敗した場合に、処理全体を失敗/失敗させたい場合はどうすればよいですか?
Terry

3
@Terryは、(によって作成されたTask.WhenAll)最上位のタスクに(内にAggregateException)例外が含まれるという意味で、呼び出し元にバブルアップします。その結果、呼び出し元がを使用した場合await、呼び出しサイトで例外がスローされます。ただし、すべてのタスクが完了するまでTask.WhenAll待機し、が呼び出されると、処理する要素がなくなるまで動的に要素を割り当てます。これは、処理を停止するための独自のメカニズム(例:)を追加しない限り、それ自体では発生しないことを意味します。GetPartitionspartition.MoveNextCancellationToken
Ohad Schneider

1
@gibbocoolまだフォローしているかわかりません。コメントで指定したパラメーターを使用して、合計7つのタスクがあるとします。さらに、最初のバッチで5秒のタスクが時々発生し、1秒のタスクが3つ発生するとします。約1秒後、5秒のタスクは引き続き実行されますが、3つの1秒のタスクは終了します。この時点で、残りの3つの1秒のタスクが実行を開始します(これらは、パーティショナーによって3つの「空き」スレッドに提供されます)。
Ohad Schneider

2
@MichaelFreidgeimを使用var current = partition.Currentするawait bodyと、以前と同じようにcurrentして、継続で使用できます(ContinueWith(t => { ... })。
Ohad Schneider 2017

43

質問が最初に投稿された4年前には存在しなかった新しいAsyncEnumerator NuGetパッケージを使用すると、労力を節約できます。並列度を制御できます。

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
セルゲイ、あなたはあなたが図書館の作者であることを開示するべきです
マイケル・フライドイム

5
はい、免責事項を追加しました。私はそれを宣伝することによる利益を求めているのではなく、ただ人々を助けたいだけです;)
セルジュ・セメノフ

ライブラリは.NET Coreと互換性がありません。
Cornielノーベル2018

2
@CornielNobel、それは.NET Coreと互換性があります-GitHubのソースコードは.NET Frameworkと.NET Coreの両方のテストカバレッジを持っています。
セルジュセメノフ2018年

1
@SergeSemenov私はあなたのライブラリをそのためにたくさん使ってきました、AsyncStreamsそして私はそれが優れていると言わざるを得ません。このライブラリを十分にお勧めすることはできません。
-WBuck

16

ラップParallel.ForeachTask.Run()し、代わりにawaitキーワードを使用[yourasyncmethod].Result

(UIスレッドをブロックしないようにTask.Runを実行する必要があります)

このようなもの:

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を持つようにバックグラウンドスレッドに全体を押してください。何か問題はありますか?たぶん、これはスリープ状態のスレッドの1つにすぎますが、短くて読みやすいコードです。
ygoe

@LonelyPixel私の唯一の問題は、それが望ましいTask.Runときに呼び出されるTaskCompletionSourceことです。
Gusdor 2016年

1
@Gusdor Curious-なぜTaskCompletionSource望ましいのですか?
Seafish

@Seafish私が答えたいと思う良い質問です。
厳しい

ほんの短い更新。私は今まさにこれを探していて、スクロールして最も簡単な解決策を見つけ、自分のコメントを再び見つけました。私はこのコードを正確に使用しましたが、期待どおりに機能します。ループ内に元の非同期呼び出しの同期バージョンがあることを前提としています。await前面に移動して、追加の変数名を保存できます。
ygoe 2017年

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))に分割し、それぞれのアイテムを1つずつ処理する10個のタスクを実行し(.SelectManyAsync(...))、それらを1つのリストにマージします。

より簡単なアプローチがあることに言及する価値があります:

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);
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.