C#
 Computer >> コンピューター >  >> プログラミング >> C#

C#でParallel.ForEachの並列実行数を制限する方法【MaxDegreeOfParallelism活用】

C#のParallel.ForEachは、複数のスレッドを使ってコレクションを並列処理できる強力な機能です。しかし、デフォルト設定では使用可能なすべてのリソースを活用しようとするため、状況によっては並列度(同時に実行されるタスク数)を制限したいケースがあります。本記事では、Parallel.ForEachと通常のforeachの違いを確認したうえで、MaxDegreeOfParallelismプロパティを使って並列度を制御する方法を解説します。

Parallel.ForEachとは

Parallel.ForEachは、複数のスレッド上でループ処理を実行し、処理を並列に進めるための仕組みです。C#の基本構文ではなく、.NET Framework 4.0以降で利用可能になった機能です。使用するには、usingディレクティブでSystem.Threading.Tasks名前空間をインポートする必要があります。

通常のforeachとの違い

一方、通常のforeachループは単一のスレッド上で動作し、要素を1つずつ順番に処理します。foreachはC# 1.0から存在する基本機能であり、ほとんどの場合、Parallel.ForEachよりも実行速度は遅くなります。ただし、処理が軽量な場合や、順序が重要な場合にはforeachの方が適していることもあります。

項目foreachParallel.ForEach
実行方式シングルスレッド・逐次処理マルチスレッド・並列処理
利用可能バージョンC# 1.0以降.NET Framework 4.0以降
必要な名前空間不要System.Threading.Tasks
処理順序の保証ありなし
速度比較的遅い多くの場合高速

サンプルコード1:foreachとParallel.ForEachの比較

まず、両者の動作の違いを実際のコードで確認してみましょう。AからZまでのアルファベットを格納したリストを、それぞれの方法で処理し、実行時間とスレッドIDを出力します。

static void Main(string[] args){
   List<string> alphabets = new List<string>();
   alphabets.Add("A");
   alphabets.Add("B");
   alphabets.Add("C");
   alphabets.Add("D");
   alphabets.Add("E");
   alphabets.Add("F");
   alphabets.Add("G");
   alphabets.Add("H");
   alphabets.Add("I");
   alphabets.Add("J");
   alphabets.Add("K");
   alphabets.Add("L");
   alphabets.Add("M");
   alphabets.Add("N");
   alphabets.Add("O");
   alphabets.Add("P");
   alphabets.Add("Q");
   alphabets.Add("R");
   alphabets.Add("S");
   alphabets.Add("T");
   alphabets.Add("U");
   alphabets.Add("V");
   alphabets.Add("W");
   alphabets.Add("X");
   alphabets.Add("Y");
   alphabets.Add("Z");
   Console.WriteLine("Printing list using foreach loop\n");
   var stopWatch = Stopwatch.StartNew();
   foreach (string alphabet in alphabets){
      Console.WriteLine("alphabet Name: {0}, Thread Id= {1}", alphabet,
         Thread.CurrentThread.ManagedThreadId);
   }
   Console.WriteLine("foreach loop execution time = {0} seconds\n", stopWatch.Elapsed.TotalSeconds);
   Console.WriteLine("Printing list using Parallel.ForEach");
   stopWatch = Stopwatch.StartNew();
   Parallel.ForEach(alphabets, alphabet =>
   {
      Console.WriteLine("alphabet Name: {0}, Thread Id= {1}", alphabet,
      Thread.CurrentThread.ManagedThreadId);
   }
   );
   Console.WriteLine("Parallel.ForEach() execution time = {0} seconds",
   stopWatch.Elapsed.TotalSeconds);
   Console.Read();
   Console.ReadLine();
}

実行結果(foreach)

通常のforeachでは、すべての要素が同じスレッド(Thread Id= 1)で順番に処理されていることがわかります。

alphabet Name: A, Thread Id= 1
alphabet Name: B, Thread Id= 1
alphabet Name: C, Thread Id= 1
alphabet Name: D, Thread Id= 1
...(中略)...
alphabet Name: Z, Thread Id= 1
foreach loop execution time = 0.0223421 seconds

実行結果(Parallel.ForEach)

Parallel.ForEachでは、複数のスレッド(Thread Id= 1、4、5、6、7など)が使用され、処理順序もバラバラになっています。これは並列処理の典型的な特徴です。

alphabet Name: A, Thread Id= 1
alphabet Name: G, Thread Id= 4
alphabet Name: H, Thread Id= 4
alphabet Name: I, Thread Id= 4
...(中略)...
alphabet Name: W, Thread Id= 6
alphabet Name: X, Thread Id= 6
Parallel.ForEach() execution time = 0.0559777 seconds

ポイント:この例では要素ごとの処理が非常に軽量なため、スレッド管理のオーバーヘッドによりParallel.ForEachの方がかえって遅くなっています。並列処理が効果を発揮するのは、画像処理やAPI呼び出しなど、1件あたりの処理が重い場合です。

サンプルコード2:MaxDegreeOfParallelismで並列度を制限する

Parallel.ForEachの並列度を制限するには、ParallelOptionsクラスのMaxDegreeOfParallelismプロパティを使用します。以下の例では、同時に実行される最大スレッド数を「2」に制限しています。

static class Program{
   static void Main(string[] args){
      List<string> alphabets = new List<string>();
      alphabets.Add("A");
      alphabets.Add("B");
      alphabets.Add("C");
      alphabets.Add("D");
      alphabets.Add("E");
      alphabets.Add("F");
      alphabets.Add("G");
      alphabets.Add("H");
      alphabets.Add("I");
      alphabets.Add("J");
      alphabets.Add("K");
      alphabets.Add("L");
      alphabets.Add("M");
      alphabets.Add("N");
      alphabets.Add("O");
      alphabets.Add("P");
      alphabets.Add("Q");
      alphabets.Add("R");
      alphabets.Add("S");
      alphabets.Add("T");
      alphabets.Add("U");
      alphabets.Add("V");
      alphabets.Add("W");
      alphabets.Add("X");
      alphabets.Add("Y");
      alphabets.Add("Z");
      Parallel.ForEach(
      alphabets,
      new ParallelOptions { MaxDegreeOfParallelism = 2 },
      alphabet => { Console.WriteLine("alphabet Name: {0}, Thread Id= {1}", alphabet,
      Thread.CurrentThread.ManagedThreadId); }
   );
   }
}

実行結果

実行結果を見ると、使用されているスレッドはメインスレッド(Thread Id= 1)とワーカースレッド(Thread Id= 4)の2つだけであることが確認できます。これにより、同時実行数が確実に2に制限されています。

alphabet Name: N, Thread Id= 4
alphabet Name: O, Thread Id= 4
alphabet Name: P, Thread Id= 4
alphabet Name: A, Thread Id= 1
alphabet Name: B, Thread Id= 1
alphabet Name: C, Thread Id= 1
...(中略)...
alphabet Name: M, Thread Id= 4

MaxDegreeOfParallelismの設定値について

  • -1(デフォルト):並列度を自動的に設定します。.NETランタイムがCPUのコア数などを考慮して最適な値を選択します。
  • 正の整数:同時に実行される操作の最大数を指定します。「2」なら最大2つの処理が同時に走ります。
  • 注意点:この値はあくまで「上限」です。指定した数のスレッドが必ず常時使われるわけではありません。

まとめ

Parallel.ForEachで並列度を制限したい場合は、第2引数にnew ParallelOptions { MaxDegreeOfParallelism = n }を渡すだけで実現できます。外部APIへのアクセス制限やデータベース接続プールの枯渇防止、CPU負荷の抑制など、リソースに制約がある場面で特に有効なテクニックです。ただし、軽量な処理に対して無闇に並列化するとオーバーヘッドで逆に遅くなることもあるため、処理の重さに応じて適切に使い分けることが重要です。

  1. データバックアップの正しい方法とは?3-2-1ルールで大切なデータを守る

    現代のIT社会において、包括的なバックアップ戦略を持つことは不可欠です。データが失われる原因は数多く存在し、バックアップを適切に行う方法を理解することは、深刻な事態を回避するために極めて重要です。では、具体的にどのようにデータをバックアップすればよいのでしょうか?データ損失のリスクサイバー攻撃、内部犯行、自然災害、記録メディアの破損、人的ミスなど、データを失う要因は枚挙にいとまがありません。個人にとってデータ損失は煩わしく心を痛める出来事ですが、企業にとっては取り返しのつかない結果をもたらしかねません。Consoltechによる以下の衝撃的な統計をご覧ください。重大なデータ損失を経験した企業の

  2. ウイルス対策ソフトでPCを高速化する方法|セキュリティと快適さを両立させる5つのポイント

    「アンチウイルスソフトを入れるとPCが遅くなる」という誤解を持っている方は少なくありません。確かに、まれに一時的に動作が重く感じられることはありますが、それはPC全体を徹底的にスキャンし、脅威を早期に排除するために必要な処理です。むしろ総合的に見れば、アンチウイルスソフトはPCのパフォーマンス向上に大きく貢献してくれます。 現在、優れた有料・無料のアンチウイルスソフトが数多く登場しています。本記事では、その中でも特に評判の高い「Systweak Antivirus」を例に挙げて解説します。詳細なレビュー記事も公開しているので、実際の動作を確認したい方はぜひ参考にしてください。 アンチウイルスソ