recursiveaction ejemplos ejemplo docs java multithreading fork-join forkjoinpool work-stealing

docs - fork join java ejemplos



Java ForkJoinPool con tareas no recursivas, ¿funciona el robo de trabajo? (1)

Quiero enviar tareas ejecutables a ForkJoinPool a través de un método:

forkJoinPool.submit(Runnable task)

Tenga en cuenta, yo uso JDK 7.

Bajo el capó, se transforman en objetos ForkJoinTask. Sé que ForkJoinPool es eficiente cuando una tarea se divide en tareas más pequeñas de forma recursiva.

Pregunta:

¿El robo de trabajo sigue funcionando en ForkJoinPool si no hay recursión?

¿Vale la pena en este caso?

Actualización 1: Las tareas son pequeñas y pueden estar desequilibradas. Incluso para tareas estrictamente iguales, cosas como el cambio de contexto, la programación de subprocesos, el estacionamiento, las páginas que faltan, etc. se interponen en el camino que conduce al desequilibrio .

Actualización 2: Doug Lea escribió en el grupo de interés Concurrencia JSR-166 , dando una pista sobre esto:

Esto también mejora en gran medida el rendimiento cuando todas las tareas son asíncronas y se envían al grupo en lugar de bifurcarse, lo que se convierte en una forma razonable de estructurar los marcos de actores, así como muchos servicios simples para los que de otra forma podría usar ThreadPoolExecutor.

Supongo que, cuando se trata de tareas ligadas a la CPU razonablemente pequeñas, ForkJoinPool es el camino a seguir, gracias a esta optimización. El punto principal es que estas tareas ya son pequeñas y no necesitan una descomposición recursiva. El robo de trabajo funciona, sin importar si es una tarea grande o pequeña: las tareas pueden ser tomadas por otro trabajador libre de la cola de Deque de un trabajador ocupado.

Actualización 3: Escalabilidad de ForkJoinPool - la evaluación comparativa realizada por el equipo Akka de ping-pong muestra excelentes resultados.

A pesar de esto, para aplicar ForkJoinPool de manera más eficiente se requiere un ajuste del rendimiento.


ForkJoinPool código fuente de ForkJoinPool tiene una buena sección llamada "Información general sobre la implementación", que se puede leer para obtener una verdad definitiva. La explicación a continuación es mi entendimiento para JDK 8u40.

Desde el primer día, ForkJoinPool tenía una cola de trabajo por subproceso de trabajo (llamémoslos "colas de trabajo"). Las tareas bifurcadas se insertan en la cola del trabajador local, listas para que el trabajador las abra de nuevo y las ejecute; en otras palabras, parece una pila desde la perspectiva de los hilos del trabajador. Cuando un trabajador agota su cola de trabajo, va y trata de robar las tareas de otras colas de trabajo. Eso es "robo de trabajo" .

Ahora, antes de (IIRC) JDK 7u12, ForkJoinPool tenía una única cola de envío global. Cuando los subprocesos de trabajo se quedaron sin tareas locales, así como las tareas para robar, llegaron allí e intentaron ver si había trabajo externo disponible. En este diseño, no hay ninguna ventaja frente a un ThreadPoolExecutor respaldado por ArrayBlockingQueue .

Cambió significativamente después de entonces. Después de que se identificó esta cola de envío como el grave cuello de botella en el rendimiento, Doug Lea et al. Rayó las colas de envío también. En retrospectiva, esa es una idea obvia: puede reutilizar la mayoría de los mecanismos disponibles para las colas de trabajo. Incluso puede distribuir estas colas de envío por subprocesos de trabajo. Ahora, el envío externo entra en una de las colas de envío. Luego, los trabajadores que no tienen trabajo para comer, primero pueden mirar en la cola de envío asociada con un trabajador en particular, y luego pasear y mirar las colas de envío de otros. Uno puede llamar a eso "robo de trabajo" también.

He visto muchas cargas de trabajo que se benefician de esto. Esta ventaja de diseño particular de ForkJoinPool incluso para tareas simples no recursivas fue reconocida hace mucho tiempo. Muchos usuarios en concurrency-interest @ solicitaron un ejecutor de robo de trabajo simple sin todo el ForkJoinPool ForkJoinPool. Esta es una de las razones por las que tenemos Executors.newWorkStealingPool() en JDK 8 en adelante: actualmente ForkJoinPool en ForkJoinPool , pero ForkJoinPool abiertos para proporcionar una implementación más sencilla.