CyclicBarrier в Java

CyclicBarrier в Java

1. Вступление

CyclicBarriers - это конструкции синхронизации, которые были введены в Java 5 как часть пакетаjava.util.concurrent.

В этой статье мы рассмотрим эту реализацию на примере параллелизма.

2. Параллелизм Java - Синхронизаторы

Пакетjava.util.concurrent содержит несколько классов, которые помогают управлять набором потоков, которые взаимодействуют друг с другом. Некоторые из них включают в себя:

  • CyclicBarrier

  • Phaser

  • CountDownLatch

  • Обменник

  • семафор

  • SynchronousQueue

Эти классы предлагают готовые функциональные возможности для общих шаблонов взаимодействия между потоками. Если у нас есть набор потоков, которые общаются друг с другом и следуют одному или нескольким более распространенным

Если у нас есть набор потоков, которые взаимодействуют друг с другом и напоминают один из общих шаблонов,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.с

При желании мы можем передать второй аргумент конструктору, который является экземпляромRunnable. Это логика, которая будет выполняться последним потоком, который отключает барьер:

public CyclicBarrier(int parties, Runnable barrierAction)

5. Реализация

Чтобы увидетьCyclicBarrier в действии, рассмотрим следующий сценарий:

Есть операция, которую выполняет фиксированное количество потоков и сохраняет соответствующие результаты в списке. Когда все потоки завершают выполнение своих действий, один из них (обычно последний, который отключает барьер) начинает обрабатывать данные, которые были получены каждым из них.

Давайте реализуем основной класс, в котором происходит все действие:

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

В приведенном выше коде мы инициализировали циклический барьер с 5 потоками, каждый из которых выдает 3 целых числа как часть своих вычислений и сохраняет их в результирующем списке.

Как только барьер отключен, последний поток, который отключил барьер, выполняет логику, указанную в AggregatorThread, а именно - добавляет все числа, созданные потоками.

6. Результаты

Вот результат одного выполнения вышеуказанной программы - каждое выполнение может создавать разные результаты, поскольку потоки могут порождаться в другом порядке:

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.