Concurrencia de Java: locking de cuenta regresiva vs barrera cíclica

Estaba leyendo la API de java.util.concurrent , y encontré que

  • CountDownLatch : un CountDownLatch sincronización que permite que uno o más subprocesos esperen hasta que se complete un conjunto de operaciones en otros subprocesos.
  • CyclicBarrier : un CyclicBarrier sincronización que permite que un conjunto de hilos espere unos a otros para alcanzar un punto de barrera común.

Para mí, ambos parecen iguales, pero estoy seguro de que hay mucho más.

Por ejemplo, en CoundownLatch, the countdown value could not be reset, that can happen in the case of CyclicBarrier .

¿Hay alguna otra diferencia entre los dos?
¿Cuáles son los use cases en los que alguien querría restablecer el valor de la cuenta atrás?

Una diferencia importante es que CyclicBarrier toma una tarea Runnable (opcional) que se ejecuta una vez que se cumple la condición de barrera común.

También le permite obtener la cantidad de clientes esperando en la barrera y el número requerido para activar la barrera. Una vez activada, la barrera se reinicia y puede usarse nuevamente.

Para casos de uso simple – servicios que comienzan, etc. … un CountdownLatch está bien. Un CyclicBarrier es útil para tareas de coordinación más complejas. Un ejemplo de esto sería el cómputo paralelo, donde múltiples subtareas están involucradas en el cálculo, algo así como MapReduce .

Hay otra diferencia.

Cuando se utiliza un CyclicBarrier , se supone que se especifica el número de hilos en espera que activan la barrera. Si especifica 5, debe tener al menos 5 hilos para llamar a await() .

Al usar un CountDownLatch , usted especifica el número de llamadas a countDown() que darán lugar a que se countDown() todos los hilos en espera. Esto significa que puede usar CountDownLatch con solo un hilo.

“¿Por qué harías eso?”, Puedes decir. Imagine que está utilizando una API misteriosa codificada por otra persona que realiza devoluciones de llamada. Desea que uno de sus subprocesos espere hasta que se haya llamado a una callback determinada varias veces. No tiene idea de a qué subprocesos se llamará la callback. En este caso, un CountDownLatch es perfecto, mientras que no puedo pensar en ninguna forma de implementarlo usando un CyclicBarrier (en realidad, puedo, pero implica tiempos de espera … ¡puaj!).

¡Solo desearía que CountDownLatch pudiera reiniciarse!

Un punto que nadie ha mencionado aún es que, en un CyclicBarrier , si un hilo tiene un problema (tiempo de espera, interrupción …), todos los demás que han alcanzado await() obtienen una excepción. Ver Javadoc:

El CyclicBarrier utiliza un modelo de rotura todo o nada para bashs fallidos de sincronización: si un hilo sale prematuramente de un punto de barrera debido a interrupción, falla o tiempo de espera, todos los otros hilos que esperan en ese punto de barrera también saldrán anormalmente a través de BrokenBarrierException (o InterruptedException si ellos también fueron interrumpidos aproximadamente al mismo tiempo).

Creo que JavaDoc ha explicado las diferencias de forma explícita. La mayoría de la gente sabe que CountDownLatch no se puede restablecer, sin embargo, CyclicBarrier sí puede. Pero esta no es la única diferencia, o CyclicBarrier podría renombrarse como ResetbleCountDownLatch. Deberíamos contar las diferencias desde la perspectiva de sus objectives, que se describen en JavaDoc

CountDownLatch: un asistente de sincronización que permite que uno o más subprocesos esperen hasta que se complete un conjunto de operaciones en otros subprocesos.

CyclicBarrier: un asistente de sincronización que permite que un conjunto de hilos espere unos a otros para alcanzar un punto de barrera común.

En countDownLatch, hay uno o más subprocesos, que están esperando que se complete un conjunto de otros subprocesos . En esta situación, hay dos tipos de subprocesos, un tipo está esperando, otro tipo está haciendo algo, una vez que finaliza sus tareas, puede estar esperando o simplemente finalizado.

En CyclicBarrier, solo hay un tipo de subprocesos, están esperando el uno al otro, son iguales.

La principal diferencia está documentada en los Javadocs para CountdownLatch. A saber:

Un CountDownLatch se inicializa con un conteo dado. El bloque de métodos de espera hasta que el recuento actual llega a cero debido a las invocaciones del método countDown (), después del cual se liberan todos los subprocesos en espera y las invocaciones posteriores de espera vuelven inmediatamente. Este es un fenómeno de una sola vez: el recuento no se puede restablecer. Si necesita una versión que restablece el recuento, considere usar un CyclicBarrier.

fuente 1.6 Javadoc

Esta pregunta ya ha recibido una respuesta adecuada, pero creo que puedo agregar algo de valor publicando algún código.

Para ilustrar el comportamiento de la barrera cíclica, he hecho algunos ejemplos de código. Tan pronto como la barrera se vuelque, se restablece automáticamente para que pueda volver a utilizarse (por lo tanto, es “cíclica”). Cuando ejecute el progtwig, observe que las impresiones “Let’s play” se activan solo después de que se da una propina.

 import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierCycles { static CyclicBarrier barrier; public static void main(String[] args) throws InterruptedException { barrier = new CyclicBarrier(3); new Worker().start(); Thread.sleep(1000); new Worker().start(); Thread.sleep(1000); new Worker().start(); Thread.sleep(1000); System.out.println("Barrier automatically resets."); new Worker().start(); Thread.sleep(1000); new Worker().start(); Thread.sleep(1000); new Worker().start(); } } class Worker extends Thread { @Override public void run() { try { CyclicBarrierCycles.barrier.await(); System.out.println("Let's play."); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } 

Un CountDownLatch se usa para una sincronización única. Mientras se utiliza un CountDownLatch, cualquier hilo puede llamar a countDown () tantas veces como lo desee. Los subprocesos que llaman await () se bloquean hasta que el conteo llega a cero debido a llamadas a countDown () por otros subprocesos desbloqueados. El javadoc para CountDownLatch dice:

El bloque de métodos de espera hasta que el recuento actual llega a cero debido a las invocaciones del método countDown (), después del cual se liberan todos los subprocesos en espera y las invocaciones posteriores de espera vuelven inmediatamente. …

Otro uso típico sería dividir un problema en N partes, describir cada parte con un Runnable que ejecute esa parte y realizar una cuenta atrás en el pestillo, y poner en cola todos los Runnables a un Executor. Cuando todas las subpartes estén completas, el hilo coordinador podrá pasar por alto. (Cuando los hilos deben contar atrás repetidamente de esta manera, en su lugar use un CyclicBarrier).

Por el contrario, la barrera cíclica se utiliza para puntos de sincronización múltiples, por ejemplo, si un conjunto de subprocesos está ejecutando un cálculo en bucle / en fase y necesita sincronizarse antes de comenzar la siguiente iteración / fase. Según el javadoc para CyclicBarrier :

La barrera se llama cíclica porque puede reutilizarse después de que se liberan los hilos de espera.

A diferencia de CountDownLatch, cada llamada a await () pertenece a alguna fase y puede hacer que la secuencia se bloquee hasta que todas las partes que pertenecen a esa fase hayan invocado await (). No hay una operación explícita de countDown () soportada por CyclicBarrier.

En pocas palabras , solo para entender las diferencias funcionales clave entre los dos:

 public class CountDownLatch { private Object mutex = new Object(); private int count; public CountDownLatch(int count) { this.count = count; } public void await() throws InterruptedException { synchronized (mutex) { while (count > 0) { mutex.wait(); } } } public void countDown() { synchronized (mutex) { if (--count == 0) mutex.notifyAll(); } } } 

y

 public class CyclicBarrier { private Object mutex = new Object(); private int count; public CyclicBarrier(int count) { this.count = count; } public void await() throws InterruptedException { synchronized (mutex) { count--; while(count > 0) mutex.wait(); mutex.notifyAll(); } } } 

excepto, por supuesto, características como no locking, tiempo de espera, diagnóstico y todo lo que se ha explicado detalladamente en las respuestas anteriores.

Sin embargo, las clases anteriores son totalmente funcionales y equivalentes, dentro de la funcionalidad proporcionada, a sus homónimos corresponsales.

En una nota diferente, las subclases de clase interna de CyclicBarrier , AQS , mientras que CyclicBarrier usa ReentrantLock (mi sospecha es que podría ser de otra manera o ambas podrían usar AQS o ambas usar Lock, sin pérdida de rendimiento)

Cuando estaba estudiando sobre cierres y barreras cíclicas, se me ocurrieron estas metáforas. Barreras cíclicas : imagina que una empresa tiene una sala de reuniones. Para comenzar la reunión, un cierto número de asistentes a la reunión deben asistir a la reunión (para hacerlo oficial). el siguiente es el código de un asistente de reunión normal (un empleado)

 class MeetingAtendee implements Runnable { CyclicBarrier myMeetingQuorumBarrier; public MeetingAtendee(CyclicBarrier myMileStoneBarrier) { this.myMeetingQuorumBarrier = myMileStoneBarrier; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " i joined the meeting ..."); myMeetingQuorumBarrier.await(); System.out.println(Thread.currentThread().getName()+" finally meeting stared ..."); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { System.out.println("Meeting canceled! every body dance "); } } } 

el empleado se une a la reunión, espera a que otros vengan para comenzar la reunión. también se sale de él si la reunión se cancela 🙂 entonces tenemos a THE BOSS de cómo a las dosis no les gusta esperar a que aparezcan otros y si pierde a su paciente, cancela la reunión.

 class MeetingAtendeeTheBoss implements Runnable { CyclicBarrier myMeetingQuorumBarrier; public MeetingAtendeeTheBoss(CyclicBarrier myMileStoneBarrier) { this.myMeetingQuorumBarrier = myMileStoneBarrier; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + "I am THE BOSS - i joined the meeting ..."); //boss dose not like to wait too much!! he/she waits for 2 seconds and we END the meeting myMeetingQuorumBarrier.await(1,TimeUnit.SECONDS); System.out.println(Thread.currentThread().getName()+" finally meeting stared ..."); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { System.out.println("what WHO canceled The meeting"); } catch (TimeoutException e) { System.out.println("These employees waste my time!!"); } } } 

En un día normal, los empleados acuden a la reunión para esperar a que aparezcan otros y, si algunos de los asistentes no llegan, tienen que esperar indefinidamente. en alguna reunión especial el jefe viene y no le gusta esperar. (5 personas necesitan comenzar la reunión pero solo llega el jefe y también un empleado entusiasta) por lo que cancela la reunión (enojado)

 CyclicBarrier meetingAtendeeQuorum = new CyclicBarrier(5); Thread atendeeThread = new Thread(new MeetingAtendee(meetingAtendeeQuorum)); Thread atendeeThreadBoss = new Thread(new MeetingAtendeeTheBoss(meetingAtendeeQuorum)); atendeeThread.start(); atendeeThreadBoss.start(); 

Salida:

 //Thread-1I am THE BOSS - i joined the meeting ... // Thread-0 i joined the meeting ... // These employees waste my time!! // Meeting canceled! every body dance  

Hay otro escenario en el que otro hilo outsider (un terremoto) cancela la reunión (método de restablecimiento de llamada). en este caso, todos los hilos de espera se despiertan con una excepción.

 class NaturalDisasters implements Runnable { CyclicBarrier someStupidMeetingAtendeeQuorum; public NaturalDisasters(CyclicBarrier someStupidMeetingAtendeeQuorum) { this.someStupidMeetingAtendeeQuorum = someStupidMeetingAtendeeQuorum; } void earthQuakeHappening(){ System.out.println("earth quaking....."); someStupidMeetingAtendeeQuorum.reset(); } @Override public void run() { earthQuakeHappening(); } } 

el código de ejecución dará como resultado resultados divertidos:

 // Thread-1I am THE BOSS - i joined the meeting ... // Thread-0 i joined the meeting ... // earth quaking..... // what WHO canceled The meeting // Meeting canceled! every body dance  

También puede agregar una secretaria a la sala de reuniones, si se lleva a cabo una reunión ella documentará todo, pero ella no es parte de la reunión:

 class MeetingSecretary implements Runnable { @Override public void run() { System.out.println("preparing meeting documents"); System.out.println("taking notes ..."); } } 

Pestillos : si el jefe enojado quiere organizar una exposición para clientes de la compañía, todo debe estar listo (recursos). proporcionamos una lista de cosas por hacer cada trabajador (subproceso) dosifica su trabajo y verificamos la lista de tareas pendientes (algunos trabajadores pintan, otros preparan el sistema de sonido …). cuando todos los elementos en la lista de tareas están completos (se proporcionan recursos) podemos abrir las puertas a los clientes.

 public class Visitor implements Runnable{ CountDownLatch exhibitonDoorlatch = null; public Visitor (CountDownLatch latch) { exhibitonDoorlatch = latch; } public void run() { try { exhibitonDoorlatch .await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("customer visiting exebition"); } } 

Y los trabajadores cómo están preparando la exposición:

 class Worker implements Runnable { CountDownLatch myTodoItem = null; public Worker(CountDownLatch latch) { this.myTodoItem = latch; } public void run() { System.out.println("doing my part of job ..."); System.out.println("My work is done! remove it from todo list"); myTodoItem.countDown(); } } CountDownLatch preperationTodoList = new CountDownLatch(3); // exhibition preparation workers Worker electricalWorker = new Worker(preperationTodoList); Worker paintingWorker = new Worker(preperationTodoList); // Exhibition Visitors ExhibitionVisitor exhibitionVisitorA = new ExhibitionVisitor(preperationTodoList); ExhibitionVisitor exhibitionVisitorB = new ExhibitionVisitor(preperationTodoList); ExhibitionVisitor exhibitionVisitorC = new ExhibitionVisitor(preperationTodoList); new Thread(electricalWorker).start(); new Thread(paintingWorker).start(); new Thread(exhibitionVisitorA).start(); new Thread(exhibitionVisitorB).start(); new Thread(exhibitionVisitorC).start(); 

En el caso de CyclicBarrier, tan pronto como TODOS los hilos secundarios comiencen a llamar a barrier.await (), Runnable se ejecuta en la barrera. La barrera que aguarda en cada hilo secundario tendrá una duración diferente para finalizar, y todos terminan al mismo tiempo.

Una diferencia obvia es que solo N threads puede esperar en un CyclicBarrier of N para ser lanzado en un ciclo. Pero un número ilimitado de subprocesos puede esperar en un CountDownLatch de N. El decremento de la cuenta atrás se puede hacer con un hilo N veces o N hilos una vez cada uno o combinaciones.

En CountDownLatch , los hilos principales esperan otros hilos para completar su ejecución. En CyclicBarrier , los subprocesos de trabajo esperan el uno al otro para completar su ejecución.

No puede reutilizar la misma instancia CountDownLatch una vez que el conteo llega a cero y el locking está abierto; por otro lado, CyclicBarrier puede reutilizarse reiniciando Barrera, una vez que la barrera se ha roto.