CyclicBarrier em Java
1. Introdução
CyclicBarriers são construções de sincronização que foram introduzidas com Java 5 como parte do pacotejava.util.concurrent.
Neste artigo, exploraremos essa implementação em um cenário de simultaneidade.
2. Simultaneidade Java - Sincronizadores
O pacotejava.util.concurrent contém várias classes que ajudam a gerenciar um conjunto de threads que colaboram entre si. Alguns destes incluem:
-
CyclicBarrier
-
Phaser
-
CountDownLatch
-
Permutador
-
Semáforo
-
SynchronousQueue
Essas classes oferecem funcionalidade pronta para uso para padrões de interação comuns entre threads. Se tivermos um conjunto de threads que se comunicam entre si e seguem um ou mais dos mais comuns
Se tivermos um conjunto de threads que se comunicam entre si e se assemelham a um dos padrões comuns,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 objectse a palavra-chavesynchronized.
Vamos nos concentrar emCyclicBarrier daqui para frente.
3. CyclicBarrier
UmCyclicBarrier é um sincronizador que permite que um conjunto de threads espere um pelo outro para atingir um ponto de execução comum, também chamado debarrier.
CyclicBarriers são usados em programas nos quais temos um número fixo de threads que devem esperar uns pelos outros para atingir um ponto comum antes de continuar a execução.
A barreira é chamada decyclic porque pode ser reutilizada após a liberação dos threads em espera.
4. Uso
O construtor para aCyclicBarrier é simples. É necessário um único inteiro que denota o número de threads que precisam chamar o métodoawait() na instância de barreira para significar o alcance do ponto de execução comum:
public CyclicBarrier(int parties)
As threads que precisam sincronizar sua execução também são chamadas departiese chamar o métodoawait() é como podemos registrar que uma determinada thread atingiu o ponto de barreira.
Essa chamada é síncrona e o encadeamento que chama esse método suspende a execução até que um número especificado de encadeamentos chame o mesmo método na barreira. This situation where the required number of threads have called await(), is called tripping the barrier.
Opcionalmente, podemos passar o segundo argumento para o construtor, que é uma instânciaRunnable. Isso tem lógica que seria executada pelo último thread que desarma a barreira:
public CyclicBarrier(int parties, Runnable barrierAction)
5. Implementação
Para verCyclicBarrier em ação, vamos considerar o seguinte cenário:
Há uma operação que executa um número fixo de threads e armazena os resultados correspondentes em uma lista. Quando todos os encadeamentos terminam de executar sua ação, um deles (normalmente o último que desarma a barreira) começa a processar os dados que foram buscados por cada um deles.
Vamos implementar a classe principal onde toda a ação acontece:
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;
// ...
}
Esta classe é bastante direta -NUM_WORKERS é o número de threads que serão executados eNUM_PARTIAL_RESULTS é o número de resultados que cada thread de trabalho irá produzir.
Finalmente, temospartialResults que são uma lista que vai armazenar os resultados de cada um desses threads de trabalho. Observe que esta lista é umSynchronizedList porque vários threads estarão gravando nela ao mesmo tempo, e o métodoadd() não é seguro para threads em umArrayList simples.
Agora vamos implementar a lógica de cada um dos threads de trabalho:
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) {
// ...
}
}
}
}
Agora vamos implementar a lógica que é executada quando a barreira foi acionada.
Para manter as coisas simples, vamos apenas adicionar todos os números na lista de resultados parciais:
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);
}
}
}
A etapa final seria construirCyclicBarriere começar com um métodomain():
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);
}
}
No código acima, inicializamos a barreira cíclica com 5 threads, cada um produzindo 3 números inteiros como parte de sua computação e armazenando o mesmo na lista resultante.
Depois que a barreira é disparada, o último encadeamento que disparou a barreira executa a lógica especificada no AggregatorThread, a saber - adicione todos os números produzidos pelos encadeamentos.
6. Resultados
Aqui está o resultado de uma execução do programa acima - cada execução pode criar resultados diferentes, pois os threads podem ser gerados em uma ordem diferente:
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
Como mostra a saída acima,Thread 4 é aquele que desarma a barreira e também executa a lógica de agregação final. Também não é necessário que os encadeamentos sejam realmente executados na ordem em que foram iniciados, como mostra o exemplo acima.
7. Conclusão
Neste artigo, vimos o que é aCyclicBarrier e em que tipo de situações ele é útil.
Também implementamos um cenário em que precisávamos de um número fixo de encadeamentos para atingir um ponto de execução fixo, antes de continuar com outra lógica do programa.
Como sempre, o código do tutorial pode ser encontradoover on GitHub.