【C#.NET解説】Barrierクラスの基本機能と使い方を徹底解説 スレッドの同期を効率化する方法

2024年12月12日木曜日

スレッド

t f B! P L

名前空間 System.Threading

Barrier

自己紹介

ようこそ、訪問者の皆さま。「Barrier」は、スレッドの同期ポイントを設定するためのクラスです。複数のスレッドが一定のタイミングで一緒に次の処理に進むように制御する役割を持っています。例えば、大きなタスクをいくつかのフェーズに分割し、それぞれのフェーズでスレッド間の同期を取るといった場面で活用されます。

基本機能

「Barrier」は、複数のスレッドが特定のフェーズを終えた後に次のステージに進むための仕組みを提供します。以下のコードは、3つのスレッドが同期ポイントを使って協調的に動作する例です。

C#

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // 3つのスレッドを同期させるBarrierの作成
        Barrier barrier = new Barrier(3, b => 
        {
            Console.WriteLine($"フェーズ {b.CurrentPhaseNumber} が完了しました。");
        });

        // スレッドを開始
        for (int i = 1; i <= 3; i++)
        {
            int threadId = i;
            Thread thread = new Thread(() =>
            {
                Console.WriteLine($"スレッド {threadId} が作業中...");
                Thread.Sleep(1000 * threadId); // 作業のシミュレーション
                Console.WriteLine($"スレッド {threadId} が同期ポイントに到達しました。");
                barrier.SignalAndWait(); // バリアで待機
            });
            thread.Start();
        }
    }
}

間違った使い方

このコードでは、CalculatePartメソッド内でbarrier.SignalAndWait()が3回呼び出されるべきところを、最後のフェーズでのみ呼び出しています。このため、Barrierの数が一致せず、スレッドの同期が正しく行われません。

C#

using System;
using System.Threading;

class Program
{
    static Barrier barrier = new Barrier(3, (b) =>
    {
        Console.WriteLine($"フェーズ {b.CurrentPhaseNumber} 完了");
    });

    static void Main(string[] args)
    {
        Thread t1 = new Thread(CalculatePart);
        Thread t2 = new Thread(CalculatePart);
        Thread t3 = new Thread(CalculatePart);

        t1.Start();
        t2.Start();
        t3.Start();

        // t1, t2, t3の終了を待機
        t1.Join();
        t2.Join();
        t3.Join();

        Console.WriteLine("全てのスレッドが完了しました。次の処理を開始します。");
    }

    static void CalculatePart()
    {
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine($"スレッド {Thread.CurrentThread.ManagedThreadId} がフェーズ {i} に到達");
            Thread.Sleep(1500); // 計算のシミュレーション

            // 間違った使用例: Barrierの数が一致しない
            if (i == 2)
            {
                barrier.SignalAndWait();
            }
        }
        Console.WriteLine($"スレッド {Thread.CurrentThread.ManagedThreadId} が完了しました。");
    }
}

さまざまなメソッドの活用例

「Barrier」には、以下のような便利なメソッドがあります。これらを活用することで、柔軟な同期処理が可能です。

C#

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Barrier barrier = new Barrier(3);

        // 参加者を追加
        barrier.AddParticipant();
        Console.WriteLine($"参加者数: {barrier.ParticipantCount}");

        // 参加者を削除
        barrier.RemoveParticipant();
        Console.WriteLine($"参加者数: {barrier.ParticipantCount}");
    }
}

具体的な使い方

以下の例は、Barrierを使って複数フェーズで同期を取る例です。

C#
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

class Program
{
    static void Main()
    {
        // 読み込みたいファイルのパスリスト(例として日付順に並んだファイルを想定)
        var files = new List<string>
        {
            "file_20230101.txt",
            "file_20230102.txt",
            "file_20230103.txt"
        };

        // Barrierの初期化
        Barrier barrier = new Barrier(files.Count, 
            postPhaseAction: (b) => Console.WriteLine("すべてのスレッドがフェーズ {0} を完了しました。", b.CurrentPhaseNumber));

        // 処理結果を格納するリスト
        var results = new List<string>();
        var lockObject = new object(); // リストへのスレッドセーフな操作のためのロックオブジェクト

        // 各ファイルを処理するスレッドを作成
        var threads = files.Select(file => new Thread(() => ProcessFile(file, barrier, results, lockObject))).ToList();

        // スレッドの開始
        threads.ForEach(t => t.Start());

        // スレッドの終了を待機
        threads.ForEach(t => t.Join());

        Console.WriteLine("すべてのファイル処理が完了しました。");

        Console.WriteLine("結果一覧:");
        foreach (var result in results)
        {
            Console.WriteLine(result);
        }
    }

    static void ProcessFile(string filePath, Barrier barrier, List<string> results, object lockObject)
    {
        Console.WriteLine("{0} の読み込みを開始します。", filePath);

        // ファイルの内容を読み込む(例としてファイル名をそのまま内容とする)
        string content = File.ReadAllText(filePath);

        lock (lockObject)
        {
            results.Add(content);
        }

        Console.WriteLine("{0} の読み込みが完了しました。", filePath);

        // 他のスレッドと同期
        barrier.SignalAndWait();

        Console.WriteLine("{0} の解析を開始します。", filePath);

        // ファイルの解析処理(ここではダミーでデータの加工を行う)
        string processedContent = content.ToUpper();

        lock (lockObject)
        {
            results.Add(processedContent);
        }

        Console.WriteLine("{0} の解析が完了しました。", filePath);

        // 他のスレッドと同期
        barrier.SignalAndWait();
    }
}

このコードは、複数のファイルを日付順に読み込みながら、全てのスレッドが同期を取って次の処理フェーズに進む例を示しています。

SignalAndWaitを使用するメリット

  • 複数のスレッドが同時に異なるファイルを処理することで並列化が可能です。
  • 各処理フェーズで同期を取るため、全てのファイルが読み込み完了してから解析を開始するなどの一貫性が保証されます。

このようなシナリオでは、Barrierを活用することで、スレッド間でのデータ整合性を確保しながら効率的な処理が可能となります。

このブログを検索

QooQ