web-dev-qa-db-ja.com

Parallel.ForEachでのネストの待機

メトロアプリでは、多くの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();
149
Darthg8r

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

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

Parallel.ForEach()の代わりに TPL Dataflow を使用すると、非同期Tasksを適切にサポートできます。

具体的には、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()を使用してアイテムを非同期的に追加できます。

コードと比較した場合の追加の利点(機能した場合)は、単一の項目が終了するとすぐに書き込みが開始され、すべての処理が終了するまで待機しないことです。

148
svick

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();
111
Stephen Cleary

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

EDITFellow SO user and 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
                          });

        })); 
}
71
Ohad Schneider

新しい 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の下でライセンスされています。このメッセージは、コミュニティを支援するためだけに投稿しています。

32
Serge Semenov

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;
12
ofcoursedude

これは非常に効率的であり、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;
}
7
John Gietzen

私はパーティーに少し遅れていますが、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);
});
4
Teoman shipahi

多数のヘルパーメソッドを導入すると、次の単純な構文で並列クエリを実行できるようになります。

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

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);
2
Jay Shah