1. Введение
В этом руководстве мы сравним CyclicBarrier
и CountDownLatch
и попытаемся понять сходства и различия между ними.
2. Что это?
Когда дело доходит до параллелизма, может быть сложно понять, для чего каждый из них предназначен.
Прежде всего, и CountDownLatch, и
CyclicBarrier используются
для управления многопоточными приложениями .
И оба они предназначены для выражения того, как данный поток или группа потоков должны ожидать.
2.1. Защелка
CountDownLatch — это конструкция, которую поток ожидает
, в то время как другие потоки отсчитывают
защелку до тех пор, пока она не достигнет нуля .
``
Мы можем думать об этом как о блюде в ресторане, которое готовят. Независимо от того, какой повар приготовит сколько угодно n
блюд, официант должен дождаться
, пока все блюда не будут на тарелке. Если на тарелку помещается n
предметов, любая кухарка будет отсчитывать
защелку для каждого предмета, который она кладет на тарелку.
2.2. циклическийбарьер
CyclicBarrier — это многократно используемая конструкция, в которой группа потоков вместе ожидает
прибытия
всех
потоков . В этот момент барьер разрушается, и действие
может быть предпринято по желанию.
``
Мы можем думать об этом как о группе друзей. Каждый раз, когда они планируют поесть в ресторане, они выбирают общую точку, где они могут встретиться. Там они ждут
друг друга, и только когда все соберутся
, они смогут пойти в ресторан, чтобы вместе поесть.
2.3. Дальнейшее чтение
А для более подробной информации о каждом из них в отдельности обратитесь к нашим предыдущим руководствам по CountDownLatch
и CyclicBarrier
соответственно.
3. Задачи против потоков
Давайте углубимся в некоторые семантические различия между этими двумя классами.
Как указано в определениях, CyclicBarrier
позволяет нескольким потокам ожидать друг друга, тогда как CountDownLatch
позволяет одному или нескольким потокам ожидать завершения ряда задач.
Короче говоря, CyclicBarrier
поддерживает количество потоков
, тогда как CountDownLatch
поддерживает количество задач
.
В следующем коде мы определяем CountDownLatch
со счетчиком, равным двум. Затем мы дважды вызываем countDown()
из одного потока:
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
countDownLatch.countDown();
countDownLatch.countDown();
});
t.start();
countDownLatch.await();
assertEquals(0, countDownLatch.getCount());
Как только защелка достигает нуля, вызов await
возвращается.
Обратите внимание, что в этом случае мы смогли заставить один и тот же поток уменьшить счетчик дважды.
CyclicBarrier,
однако, отличается в этом отношении.
Как и в приведенном выше примере, мы создаем CyclicBarrier,
снова со счетчиком два, и вызываем для него await()
, на этот раз из того же потока:
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
Thread t = new Thread(() -> {
try {
cyclicBarrier.await();
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// error handling
}
});
t.start();
assertEquals(1, cyclicBarrier.getNumberWaiting());
assertFalse(cyclicBarrier.isBroken());
Первое отличие состоит в том, что ожидающие потоки сами являются барьером.
Во-вторых, и что более важно, второй метод await()
бесполезен . Один поток не может отсчитывать
барьер дважды.
В самом деле, поскольку t
должен ждать
, пока другой поток вызовет await()
— чтобы довести счет до двух, — второй вызов t
к await()
фактически не будет вызван до тех пор, пока барьер уже не будет преодолен!
В нашем тесте барьер не был преодолен, потому что у нас есть только один ожидающий поток, а не два потока, которые потребуются для отключения барьера. Это также видно из метода cycloBarrier.isBroken()
, который возвращает false
.
4. Повторное использование
Второе наиболее очевидное различие между этими двумя классами — возможность повторного использования. Чтобы уточнить, когда барьер срабатывает в CyclicBarrier
, счетчик сбрасывается до исходного значения. CountDownLatch
отличается тем, что счетчик никогда не сбрасывается.
В данном коде мы определяем CountDownLatch
со счетчиком 7 и считаем его через 20 различных вызовов:
CountDownLatch countDownLatch = new CountDownLatch(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
es.execute(() -> {
long prevValue = countDownLatch.getCount();
countDownLatch.countDown();
if (countDownLatch.getCount() != prevValue) {
outputScraper.add("Count Updated");
}
});
}
es.shutdown();
assertTrue(outputScraper.size() <= 7);
Мы наблюдаем, что хотя 20 разных потоков вызывают countDown()
, счетчик не сбрасывается, как только достигает нуля.
Как и в приведенном выше примере, мы определяем CyclicBarrier
со счетчиком 7 и ждем его от 20 разных потоков:
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
es.execute(() -> {
try {
if (cyclicBarrier.getNumberWaiting() <= 0) {
outputScraper.add("Count Updated");
}
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// error handling
}
});
}
es.shutdown();
assertTrue(outputScraper.size() > 7);
В этом случае мы наблюдаем, что значение уменьшается каждый раз, когда запускается новый поток, путем сброса к исходному значению, как только оно достигает нуля.
5. Вывод
В целом, CyclicBarrier
и CountDownLatch
являются полезными инструментами для синхронизации между несколькими потоками. Тем не менее, они принципиально отличаются с точки зрения функциональности, которую они обеспечивают. Внимательно рассмотрите каждый из них, чтобы определить, какой из них подходит для данной работы.
Как обычно, все обсуждаемые примеры доступны на Github .