Parallel.ForEachとTask.RunおよびTask.WhenAll


158

Parallel.ForEachまたはTask.Run()を使用して一連のタスクを非同期で開始する場合の違いは何ですか?

バージョン1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

バージョン2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);

3
Task.WaitAll代わりに使用した場合、2番目のコードフラグメントは1番目のものとほぼ同じになると思いますTask.WhenAll
2013年

15
また、2番目はDoSomething( "s3")を3回実行し、同じ結果を生成しないことにも注意してください。 stackoverflow.com/questions/4684320/...
Nullius


@ダン:バージョン2は非同期/待機を使用することに注意してください。つまり、別の質問です。非同期/待機は、潜在的な重複スレッドが書き込まれてから1.5年後のVS 2012で導入されました。
ペッターT

回答:


159

この場合、2番目のメソッドは、ブロックする代わりにタスクが完了するのを非同期的に待機します。

ただし、Task.Runループでの使用には欠点があります。Parallel.ForEachには、Partitioner必要以上のタスクを実行しないように作成されるがあります。 Task.Run(これを行っているため)は常にアイテムごとに1つのタスクParallelを作成しますが、クラスバッチが機能するため、作成するタスクは合計の作業アイテムよりも少なくなります。これにより、特にループ本体の項目ごとの作業量が少ない場合は、全体的なパフォーマンスが大幅に向上します。

この場合、次のように書くことで両方のオプションを組み合わせることができます。

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

これは次の短い形式でも記述できることに注意してください。

await Task.Run(() => Parallel.ForEach(strings, DoSomething));

1
すばらしい答えです。このトピックに関する優れた読書資料を教えていただけませんか。
Dimitar Dimitrov

@DimitarDimitrov一般的なTPLについては、reedcopsey.com / series / parallelism
Reed Copsey

1
Parallel.ForEachコンストラクトがアプリケーションをクラッシュさせていました。その中で重い画像処理を行っていました。ただし、Task.Run(()=> Parallel.ForEach(....));を追加すると、クラッシュしなくなりました。理由を説明できますか?並列オプションをシステムのコア数に制限することに注意してください。
monkeyjumps 2014年

3
何場合はDoSomethingありますかasync void DoSomething
Francesco Bonizzi 2016

1
どうasync Task DoSomethingですか?
Shawn Mclean 2017年

37

最初のバージョンは、呼び出し元のスレッドを同期的にブロックします(そして、その上でいくつかのタスクを実行します)。
UIスレッドの場合は、UIがフリーズします。

2番目のバージョンは、スレッドプールで非同期にタスクを実行し、完了するまで呼び出しスレッドを解放します。

使用されるスケジューリングアルゴリズムにも違いがあります。

2番目の例は、以下のように短縮できることに注意してください。

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));

2
そうじゃないのawait Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));?タスクを返すときに(待機するのではなく)問題がありました。特に、usingオブジェクトを破棄するために必要なステートメントが含まれていた場合です。
マルティン・コル

Parallel.ForEach呼び出しが原因でUIがクラッシュしましたTask.Run(()=> Parallel.ForEach(....));を追加しました。それにクラッシュを解決しました。
monkeyjumps 2014年

1

読みやすかったので、これをやった。

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);

このようにして、タスクが次々に実行されているのか、それともすべてのタスクが一度に開始されているのですか?
Vinicius Gualberto

私が知る限り、「DoSomethingAsync()」を呼び出すと、すべてが開始されます。ただし、WhenAllが呼び出されるまでは、何もブロックされていません。
クリスM.

最初の "DoSomethingAsync()"が呼び出されたときですか?
Vinicius Gualberto

1
@ChrisM。これは実行をループに戻すため、DoSomethingAsync()の最初の待機までブロックされます。同期してタスクが返される場合、すべてのコードが次々に実行され、WhenAllはすべてのタスクが完了するのを待ちます
Simon Belanger

0

Parallel.ForEachが不適切に使用されているのを見てきましたが、この質問の例が役立つと考えました。

以下のコードをコンソールアプリで実行すると、Parallel.ForEachで実行されたタスクが呼び出しスレッドをブロックしないことがわかります。結果(ポジティブまたはネガティブ)を気にしない場合は問題ありませんが、結果が必要な場合は、必ずTask.WhenAllを使用してください。

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();

            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }


        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

結果は次のとおりです。

ここに画像の説明を入力してください

結論:

タスクでParallel.ForEachを使用しても、呼び出しスレッドはブロックされません。結果が気になる場合は、必ずタスクを待ってください。

〜乾杯

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