非同期ラムダを使用した並列foreach


138

コレクションを並行して処理したいのですが、実装に問題があるので、助けが必要です。

並列ループのラムダ内で、C#でasyncとマークされたメソッドを呼び出す場合に問題が発生します。例えば:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

作成されたすべてのスレッドは事実上単なるバックグラウンドスレッドであり、Parallel.ForEach呼び出しは完了を待機しないため、問題はカウントが0で発生します。asyncキーワードを削除すると、メソッドは次のようになります。

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, item =>
{
  // some pre stuff
  var responseTask = await GetData(item);
  responseTask.Wait();
  var response = responseTask.Result;
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

それは機能しますが、待機待機時間を完全に無効にし、手動で例外処理を行う必要があります。(簡潔にするために削除されました)。

Parallel.ForEachラムダ内でawaitキーワードを使用するループを実装するにはどうすればよいですか?出来ますか?

Parallel.ForEachメソッドのプロトタイプはAction<T>asパラメーターを取りますが、非同期ラムダを待機する必要があります。


1
そのままコンパイルエラーが発生するため、2番目のコードブロックawaitからを削除するつもりだったと思いますawait GetData(item)
Josh

回答:


186

単純な並列処理が必要な場合は、次のようにできます。

var bag = new ConcurrentBag<object>();
var tasks = myCollection.Select(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
});
await Task.WhenAll(tasks);
var count = bag.Count;

もっと複雑なものが必要な場合は、Stephen ToubのForEachAsync投稿をご覧ください。


46
おそらく、調整メカニズムが必要です。これにより、1万個のネットワークリクエストなどになるアイテムと同じ数のタスクがすぐに作成されます。
usr

10
@usr Stephen Toubの記事の最後の例は、それを扱っています。
2013

@svickその最後のサンプルに戸惑っていました。大量のタスクをバッチ処理してさらに多くのタスクを作成するように見えますが、すべて一括で開始されます。
ルークプレット2017年

2
@LukePuplett dopタスクを作成し、各タスクが入力コレクションのサブセットを順番に処理します。
スビック2017年

4
@Afshin_Zavvar:結果を使用Task.Runせずに呼び出した場合await、それは単にスレッドプールに放棄された作業を投げているだけです。それはほとんどの場合間違いです。
スティーブンクリアリー2018年

74

AsyncEnumerator NuGetパッケージのParallelForEachAsync拡張メソッドを使用できます。

using Dasync.Collections;

var bag = new ConcurrentBag<object>();
await myCollection.ParallelForEachAsync(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}, maxDegreeOfParallelism: 10);
var count = bag.Count;

1
これはあなたのパッケージですか?これを数か所に投稿したのを見たことがありますか?:Dちょっと待って..あなたの名前はパッケージにあります:D +1
Piotr Kula

17
@ppumkin、はい、それは私のものです。私はこの問題を何度も何度も見てきたので、できるだけ簡単な方法で解決し、他の人たちも苦労しないようにすることにしました:)
Serge Semenov

ありがとう..それは間違いなく理にかなっていて、大きな時間を助けてくれました!
Piotr Kula

2
入力ミスがあります:maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror 2017

3
正しいスペルは確かにmaxDegreeOfParallelismですが、@ ShiranDrorのコメントに何かがあります-パッケージで変数maxDegreeOfParalellismを誤って呼び出しました(したがって、引用符で囲まれたコードは、変更するまでコンパイルされません。)
BornToCode

17

ではSemaphoreSlim、あなたは、並列制御を実現することができます。

var bag = new ConcurrentBag<object>();
var maxParallel = 20;
var throttler = new SemaphoreSlim(initialCount: maxParallel);
var tasks = myCollection.Select(async item =>
{
  try
  {
     await throttler.WaitAsync();
     var response = await GetData(item);
     bag.Add(response);
  }
  finally
  {
     throttler.Release();
  }
});
await Task.WhenAll(tasks);
var count = bag.Count;

3

ParallelForEach asyncの軽量実装。

特徴:

  1. スロットリング(最大並列度)。
  2. 例外処理(集計例外は完了時にスローされます)。
  3. メモリー効率が良い(タスクのリストを保存する必要がない)。

public static class AsyncEx
{
    public static async Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> asyncAction, int maxDegreeOfParallelism = 10)
    {
        var semaphoreSlim = new SemaphoreSlim(maxDegreeOfParallelism);
        var tcs = new TaskCompletionSource<object>();
        var exceptions = new ConcurrentBag<Exception>();
        bool addingCompleted = false;

        foreach (T item in source)
        {
            await semaphoreSlim.WaitAsync();
            asyncAction(item).ContinueWith(t =>
            {
                semaphoreSlim.Release();

                if (t.Exception != null)
                {
                    exceptions.Add(t.Exception);
                }

                if (Volatile.Read(ref addingCompleted) && semaphoreSlim.CurrentCount == maxDegreeOfParallelism)
                {
                    tcs.SetResult(null);
                }
            });
        }

        Volatile.Write(ref addingCompleted, true);
        await tcs.Task;
        if (exceptions.Count > 0)
        {
            throw new AggregateException(exceptions);
        }
    }
}

使用例:

await Enumerable.Range(1, 10000).ParallelForEachAsync(async (i) =>
{
    var data = await GetData(i);
}, maxDegreeOfParallelism: 100);

2

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

「使用」は役に立ちません。foreachループは無期限にセマフォンを待機します。問題を再現する次の単純なコードを試してください:await Enumerable.Range(1、4).ForEachAsyncConcurrent(async(i)=> {Console.WriteLine(i); throw new Exception( "test exception");}、maxDegreeOfParallelism: 2);
nicolay.anykienko 2018年

@ nicolay.anykienkoあなたは#2について正しいです。そのメモリの問題は、tasksWithThrottler.RemoveAll(x => x.IsCompleted);を追加することで解決できます。
askids 2018年

1
私は自分のコードでそれを試してみましたが、maxDegreeOfParallelismがnullでない場合、コードのデッドロックが発生します。ここでは、再現するために、すべてのコードを見ることができます:stackoverflow.com/questions/58793118/...
マッシモSavazzi
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.