JavaのCyclicBarrier

JavaのCyclicBarrier

1. 前書き

CyclicBarriersは、java.util.concurrentパッケージの一部としてJava5で導入された同期構造です。

この記事では、同時実行シナリオでこの実装について説明します。

2. Javaの同時実行性–シンクロナイザー

java.util.concurrentパッケージには、相互に連携する一連のスレッドの管理に役立ついくつかのクラスが含まれています。 これらのいくつかは次のとおりです。

  • CyclicBarrier

  • フェイザー

  • CountDownLatch

  • エクスチェンジャー

  • セマフォ

  • SynchronousQueue

これらのクラスは、スレッド間の一般的な相互作用パターンに対してすぐに使用できる機能を提供します。 相互に通信し、より一般的な1つ以上のスレッドに従う一連のスレッドがある場合

相互に通信し、一般的なパターンの1つであるwe can simply reuse the appropriate library classes (also called Synchronizers) instead of trying to come up with a custom scheme using a set of locks and condition objectsおよびsynchronizedキーワードに類似したスレッドのセットがある場合。

今後のCyclicBarrierに注目しましょう。

3. CyclicBarrier

CyclicBarrierは、スレッドのセットが相互に待機して、barrierとも呼ばれる共通の実行ポイントに到達することを可能にするシンクロナイザーです。

CyclicBarriersは、実行を続行する前に相互に共通のポイントに到達するのを待機する必要がある固定数のスレッドがあるプログラムで使用されます。

バリアは、待機中のスレッドが解放された後に再利用できるため、cyclicと呼ばれます。

4. 使用法

CyclicBarrierのコンストラクターは単純です。 共通の実行ポイントに到達したことを示すために、バリアインスタンスでawait()メソッドを呼び出す必要があるスレッドの数を示す単一の整数を取ります。

public CyclicBarrier(int parties)

実行を同期する必要のあるスレッドはpartiesとも呼ばれ、await()メソッドを呼び出すことで、特定のスレッドがバリアポイントに到達したことを登録できます。

この呼び出しは同期であり、このメソッドを呼び出すスレッドは、指定された数のスレッドがバリア上の同じメソッドを呼び出すまで実行を中断します。 This situation where the required number of threads have called await(), is called tripping the barrier.

オプションで、2番目の引数をコンストラクターに渡すことができます。これはRunnableインスタンスです。 これには、バリアをトリップする最後のスレッドによって実行されるロジックがあります。

public CyclicBarrier(int parties, Runnable barrierAction)

5. 実装

CyclicBarrierの動作を確認するために、次のシナリオを考えてみましょう。

固定数のスレッドが実行し、対応する結果をリストに格納する操作があります。 すべてのスレッドがアクションの実行を完了すると、スレッドの1つ(通常はバリアをトリップする最後のスレッド)が、これらの各スレッドによってフェッチされたデータの処理を開始します。

すべてのアクションが発生するメインクラスを実装しましょう。

public class CyclicBarrierDemo {

    private CyclicBarrier cyclicBarrier;
    private List> partialResults
     = Collections.synchronizedList(new ArrayList<>());
    private Random random = new Random();
    private int NUM_PARTIAL_RESULTS;
    private int NUM_WORKERS;

    // ...
}

このクラスは非常に単純です。NUM_WORKERSは実行されるスレッドの数であり、NUM_PARTIAL_RESULTSは各ワーカースレッドが生成する結果の数です。

最後に、これらの各ワーカースレッドの結果を格納するリストであるpartialResultsがあります。 複数のスレッドが同時に書き込みを行うため、このリストはSynchronizedListであり、add()メソッドはプレーンなArrayListではスレッドセーフではないことに注意してください。

次に、各ワーカースレッドのロジックを実装しましょう。

public class CyclicBarrierDemo {

    // ...

    class NumberCruncherThread implements Runnable {

        @Override
        public void run() {
            String thisThreadName = Thread.currentThread().getName();
            List partialResult = new ArrayList<>();

            // Crunch some numbers and store the partial result
            for (int i = 0; i < NUM_PARTIAL_RESULTS; i++) {
                Integer num = random.nextInt(10);
                System.out.println(thisThreadName
                  + ": Crunching some numbers! Final result - " + num);
                partialResult.add(num);
            }

            partialResults.add(partialResult);
            try {
                System.out.println(thisThreadName
                  + " waiting for others to reach barrier.");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                // ...
            } catch (BrokenBarrierException e) {
                // ...
            }
        }
    }

}

次に、バリアが作動したときに実行されるロジックを実装します。

簡単にするために、部分的な結果リストにすべての数値を追加してみましょう。

public class CyclicBarrierDemo {

    // ...

    class AggregatorThread implements Runnable {

        @Override
        public void run() {

            String thisThreadName = Thread.currentThread().getName();

            System.out.println(
              thisThreadName + ": Computing sum of " + NUM_WORKERS
              + " workers, having " + NUM_PARTIAL_RESULTS + " results each.");
            int sum = 0;

            for (List threadResult : partialResults) {
                System.out.print("Adding ");
                for (Integer partialResult : threadResult) {
                    System.out.print(partialResult+" ");
                    sum += partialResult;
                }
                System.out.println();
            }
            System.out.println(thisThreadName + ": Final result = " + sum);
        }
    }
}

最後のステップは、CyclicBarrierを作成し、main()メソッドで開始することです。

public class CyclicBarrierDemo {

    // Previous code

    public void runSimulation(int numWorkers, int numberOfPartialResults) {
        NUM_PARTIAL_RESULTS = numberOfPartialResults;
        NUM_WORKERS = numWorkers;

        cyclicBarrier = new CyclicBarrier(NUM_WORKERS, new AggregatorThread());

        System.out.println("Spawning " + NUM_WORKERS
          + " worker threads to compute "
          + NUM_PARTIAL_RESULTS + " partial results each");

        for (int i = 0; i < NUM_WORKERS; i++) {
            Thread worker = new Thread(new NumberCruncherThread());
            worker.setName("Thread " + i);
            worker.start();
        }
    }

    public static void main(String[] args) {
        CyclicBarrierDemo demo = new CyclicBarrierDemo();
        demo.runSimulation(5, 3);
    }
}

上記のコードでは、それぞれが計算の一部として3つの整数を生成し、結果リストに同じ整数を格納する5つのスレッドで循環バリアを初期化しました。

バリアが作動すると、バリアを作動させた最後のスレッドがAggregatorThreadで指定されたロジックを実行します。つまり、スレッドによって生成されたすべての数値を追加します。

6. 結果

上記のプログラムの1つの実行からの出力は次のとおりです。スレッドを異なる順序で生成できるため、実行ごとに異なる結果が生成される可能性があります。

Spawning 5 worker threads to compute 3 partial results each
Thread 0: Crunching some numbers! Final result - 6
Thread 0: Crunching some numbers! Final result - 2
Thread 0: Crunching some numbers! Final result - 2
Thread 0 waiting for others to reach barrier.
Thread 1: Crunching some numbers! Final result - 2
Thread 1: Crunching some numbers! Final result - 0
Thread 1: Crunching some numbers! Final result - 5
Thread 1 waiting for others to reach barrier.
Thread 3: Crunching some numbers! Final result - 6
Thread 3: Crunching some numbers! Final result - 4
Thread 3: Crunching some numbers! Final result - 0
Thread 3 waiting for others to reach barrier.
Thread 2: Crunching some numbers! Final result - 1
Thread 2: Crunching some numbers! Final result - 1
Thread 2: Crunching some numbers! Final result - 0
Thread 2 waiting for others to reach barrier.
Thread 4: Crunching some numbers! Final result - 9
Thread 4: Crunching some numbers! Final result - 3
Thread 4: Crunching some numbers! Final result - 5
Thread 4 waiting for others to reach barrier.
Thread 4: Computing final sum of 5 workers, having 3 results each.
Adding 6 2 2
Adding 2 0 5
Adding 6 4 0
Adding 1 1 0
Adding 9 3 5
Thread 4: Final result = 46

上記の出力が示すように、Thread 4はバリアをトリップし、最終的な集約ロジックも実行するものです。 また、上記の例に示すように、スレッドが実際に開始された順序で実行される必要はありません。

7. 結論

この記事では、CyclicBarrierとは何か、そしてそれがどのような状況で役立つかを見てきました。

また、他のプログラムロジックを続行する前に、固定実行ポイントに到達するために一定数のスレッドが必要なシナリオも実装しました。

いつものように、チュートリアルのコードはover on GitHubにあります。