threads thread run parallelism new example creating and java concurrency multithreading multicore

parallelism - java thread implements runnable



Obligar a varios subprocesos a utilizar varias CPU cuando estén disponibles (10)

Estoy escribiendo un programa Java que usa mucha CPU debido a la naturaleza de lo que hace. Sin embargo, muchos de ellos se pueden ejecutar en paralelo, y he hecho que mi programa tenga múltiples subprocesos. Cuando lo ejecuto, parece que solo usa una CPU hasta que necesita más y usa otra CPU. ¿Hay algo que pueda hacer en Java para forzar la ejecución de diferentes subprocesos en diferentes núcleos / CPU?


Cuando lo ejecuto, parece que solo usa una CPU hasta que necesita más y usa otra CPU. ¿Hay algo que pueda hacer en Java para forzar la ejecución de diferentes subprocesos en diferentes núcleos / CPU?

Interpreto esta parte de su pregunta como si ya hubiera abordado el problema de hacer que su aplicación sea capaz de múltiples hilos. Y a pesar de eso, no comienza a usar múltiples núcleos inmediatamente.

La respuesta a "¿hay alguna manera de forzar ...?" Es (AFAIK) no directamente. Su JVM y / o el sistema operativo host deciden cuántos subprocesos ''nativos'' usar y cómo se correlacionan dichos subprocesos con los procesadores físicos. Tienes algunas opciones para sintonizar. Por ejemplo, encontré esta página que habla sobre cómo sintonizar Java en Solaris. Y esta página habla sobre otras cosas que pueden ralentizar una aplicación de subprocesos múltiples.


Creo que este problema está relacionado con Java Parallel Proccesing Framework (JPPF). Al usar esto puede ejecutar diferentes trabajos en diferentes procesadores.


Debe escribir su programa para hacer su trabajo en la forma de una gran cantidad de Callable entregado a un ExecutorService y ejecutado con invokeAll (...).

A continuación, puede elegir una implementación adecuada en tiempo de ejecución desde la clase de ejecutores. Una sugerencia sería llamar a Executors.newFixedThreadPool () con un número que corresponda aproximadamente al número de núcleos de la CPU para mantenerse ocupado.



En primer lugar, debe probarse a sí mismo que su programa se ejecutará más rápido en múltiples núcleos. Muchos sistemas operativos se esfuerzan en ejecutar subprocesos de programa en el mismo núcleo siempre que sea posible .

Correr en el mismo núcleo tiene muchas ventajas. La memoria caché de la CPU está caliente, lo que significa que los datos para ese programa se cargan en la CPU. Los objetos de bloqueo / monitor / sincronización están en la memoria caché de la CPU, lo que significa que otras CPU no necesitan realizar operaciones de sincronización de caché en todo el bus (¡es caro!).

Una cosa que puede hacer fácilmente que su programa se ejecute en la misma CPU todo el tiempo es el uso excesivo de bloqueos y memoria compartida. Tus hilos no deberían hablar entre ellos. Con menor frecuencia, los subprocesos usan los mismos objetos en la misma memoria, con mayor frecuencia se ejecutarán en diferentes CPU. Cuanto más a menudo usan la misma memoria, más a menudo deben bloquear la espera del otro hilo.

Cuando el sistema operativo ve un bloque de hilos para otro hilo, ejecutará ese hilo en la misma CPU siempre que sea posible. Reduce la cantidad de memoria que se mueve sobre el bus entre CPU. Eso es lo que creo que está causando lo que ves en tu programa.


Hay dos formas básicas de multi-hilo en Java. Cada tarea lógica que crees con estos métodos debe ejecutarse en un núcleo nuevo cuando sea necesario y esté disponible.

Método uno: defina un objeto Runnable o Thread (que puede tomar Runnable en el constructor) y comience a ejecutarlo con el método Thread.start (). Se ejecutará en cualquier núcleo que el sistema operativo le dé, generalmente el menos cargado.

Tutorial: definición e inicio de subprocesos

Método dos: defina los objetos que implementan la interfaz Runnable (si no devuelven valores) o Callable (si lo hacen), que contienen su código de procesamiento. Pase estas tareas a un ExecutorService del paquete java.util.concurrent. La clase java.util.concurrent.Executors tiene varios métodos para crear clases estándar y útiles de ExecutorServices. Link al tutorial de los ejecutores.

A partir de la experiencia personal, los grupos de subprocesos fijos y en caché de Executors son muy buenos, aunque querrás modificar el número de subprocesos. Runtime.getRuntime (). AvailableProcessors () se pueden usar en tiempo de ejecución para contar los núcleos disponibles. Deberá cerrar los grupos de subprocesos cuando la aplicación finalice; de ​​lo contrario, la aplicación no se cerrará porque los subprocesos de ThreadPool se mantienen en ejecución.

Obtener un buen rendimiento multinúcleo a veces es complicado y está lleno de errores:

  • La E / S de disco se ralentiza MUCHO cuando se ejecuta en paralelo. Solo un hilo debe hacer lectura / escritura de disco a la vez.
  • La sincronización de objetos proporciona seguridad a las operaciones de subprocesos múltiples, pero ralentiza el trabajo.
  • Si las tareas son demasiado triviales (pequeños bits de trabajo, ejecute rápidamente), la sobrecarga de administrarlos en un ExecutorService cuesta más de lo que gana con múltiples núcleos.
  • La creación de nuevos objetos Thread es lenta. ExecutorServices intentará reutilizar los hilos existentes si es posible.
  • Todo tipo de cosas locas pueden suceder cuando varios hilos funcionan en algo. Mantenga su sistema simple e intente hacer tareas lógicamente distintas y que no interactúen.

Otro problema: ¡controlar el trabajo es difícil! Una buena práctica es tener un hilo de administrador que crea y envía tareas, y luego un par de hilos de trabajo con colas de trabajo (utilizando un ExecutorService).

Solo estoy tocando puntos clave aquí: muchos expertos consideran que la programación multiproceso es uno de los temas de programación más difíciles. No es intuitivo, complejo, y las abstracciones a menudo son débiles.

Editar - Ejemplo usando ExecutorService:

public class TaskThreader { class DoStuff implements Callable { Object in; public Object call(){ in = doStep1(in); in = doStep2(in); in = doStep3(in); return in; } public DoStuff(Object input){ in = input; } } public abstract Object doStep1(Object input); public abstract Object doStep2(Object input); public abstract Object doStep3(Object input); public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); ArrayList<Callable> tasks = new ArrayList<Callable>(); for(Object input : inputs){ tasks.add(new DoStuff(input)); } List<Future> results = exec.invokeAll(tasks); exec.shutdown(); for(Future f : results) { write(f.get()); } } }


Lo más fácil es dividir su programa en múltiples procesos. El sistema operativo los asignará a través de los núcleos.

Algo más difícil es dividir su programa en varios hilos y confiar en que la JVM los asigne correctamente. Esto es, generalmente, lo que hacen las personas para hacer uso del hardware disponible.

Editar

¿Cómo puede un programa de multiprocesamiento ser "más fácil"? Aquí hay un paso en la tubería.

public class SomeStep { public static void main( String args[] ) { BufferedReader stdin= new BufferedReader( System.in ); BufferedWriter stdout= new BufferedWriter( System.out ); String line= stdin.readLine(); while( line != null ) { // process line, writing to stdout line = stdin.readLine(); } } }

Cada paso en la tubería está estructurado de manera similar. 9 líneas de sobrecarga para cualquier procesamiento incluido.

Esto puede no ser el absoluto más eficiente. Pero es muy fácil.

La estructura general de sus procesos concurrentes no es un problema de JVM. Es un problema de sistema operativo, entonces usa el shell.

java -cp pipline.jar FirstStep | java -cp pipline.jar SomeStep | java -cp pipline.jar LastStep

Lo único que queda es resolver alguna serialización para sus objetos de datos en la tubería. La serialización estándar funciona bien. Lea http://java.sun.com/developer/technicalArticles/Programming/serialization/ para obtener sugerencias sobre cómo serializar. Puede reemplazar BufferedReader y BufferedWriter con ObjectInputStream y ObjectOutputStream para lograr esto.



Primero, sugiero leer "Concurrency in Practice" de Brian Goetz .

Este es de lejos el mejor libro que describe la programación concurrente de Java.

La concurrencia es "fácil de aprender, difícil de dominar". Sugeriría leer mucho sobre el tema antes de intentarlo. Es muy fácil hacer que un programa de subprocesos múltiples funcione correctamente el 99.9% de las veces y fallar el 0.1%. Sin embargo, aquí hay algunos consejos para comenzar:

Hay dos formas comunes de hacer que un programa use más de un núcleo:

  1. Haz que el programa se ejecute usando múltiples procesos. Un ejemplo es Apache compilado con Pre-Fork MPM, que asigna solicitudes a procesos secundarios. En un programa multiproceso, la memoria no se comparte de manera predeterminada. Sin embargo, puede asignar secciones de memoria compartida a través de procesos. Apache hace esto con su ''marcador''.
  2. Haga el programa de subprocesos múltiples. En un programa de subprocesos múltiples, toda la memoria del montón se comparte de forma predeterminada. Cada hilo todavía tiene su propia pila, pero puede acceder a cualquier parte del montón. Por lo general, la mayoría de los programas de Java tienen múltiples subprocesos y no procesos múltiples.

En el nivel más bajo, uno puede crear y destruir hilos . Java facilita la creación de hilos de manera cruzada portátil.

Como tiende a ser costoso crear y destruir subprocesos todo el tiempo, Java ahora incluye Executors para crear grupos de subprocesos reutilizables. Las tareas se pueden asignar a los ejecutores, y el resultado se puede recuperar a través de un objeto Future.

Por lo general, uno tiene una tarea que se puede dividir en tareas más pequeñas, pero los resultados finales deben reunirse de nuevo. Por ejemplo, con un tipo de combinación, uno puede dividir la lista en partes cada vez más pequeñas, hasta que uno tenga cada núcleo haciendo la clasificación. Sin embargo, como cada sublista está ordenada, debe fusionarse para obtener la lista ordenada final. Dado que este problema de "divide y vencerás" es bastante común, existe un marco JSR que puede manejar la distribución subyacente y unirse. Este marco probablemente se incluirá en Java 7.


Puede utilizar la API a continuación de los Executors con la versión de Java 8

public static ExecutorService newWorkStealingPool()

Crea un grupo de subprocesos de robo de trabajo utilizando todos los procesadores disponibles como su nivel de paralelismo de destino.

Debido al mecanismo de robo de trabajo, los subprocesos inactivos roban tareas de la cola de tareas de subprocesos ocupados y el rendimiento general aumentará.

Desde grepcode , la implementación de newWorkStealingPool es la siguiente

/** * Creates a work-stealing thread pool using all * {@link Runtime#availableProcessors available processors} * as its target parallelism level. * @return the newly created thread pool * @see #newWorkStealingPool(int) * @since 1.8 */ public static ExecutorService newWorkStealingPool() { return new ForkJoinPool (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }