java - termino - ¿Por qué crear un Thread dice que es costoso?
runnable java (6)
Los tutoriales de Java dicen que crear un hilo es costoso. Pero, ¿por qué exactamente es caro? ¿Qué está sucediendo exactamente cuando se crea un subproceso Java que hace que su creación sea costosa? Estoy tomando la declaración como verdadera, pero estoy interesado en la mecánica de la creación de subprocesos en JVM.
Sobrecarga del ciclo de vida del hilo. La creación y el desmontaje de subprocesos no son gratuitos. La sobrecarga real varía según las plataformas, pero la creación de subprocesos lleva tiempo, introduce latencia en el procesamiento de solicitudes y requiere alguna actividad de procesamiento por parte de la JVM y el sistema operativo. Si las solicitudes son frecuentes y livianas, como en la mayoría de las aplicaciones de servidor, crear un nuevo hilo para cada solicitud puede consumir recursos informáticos significativos.
Desde Java Concurrencia en la práctica
Por Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Imprimir ISBN-10: 0-321-34960-1
En teoría, esto depende de la JVM. En la práctica, cada hilo tiene una cantidad relativamente grande de memoria de pila (256 KB por defecto, creo). Además, los subprocesos se implementan como subprocesos del sistema operativo, por lo que crearlos implica una llamada al sistema operativo, es decir, un cambio de contexto.
Tenga en cuenta que "costoso" en informática es siempre muy relativo. La creación de subprocesos es muy costosa en relación con la creación de la mayoría de los objetos, pero no es muy costosa en comparación con una búsqueda aleatoria de discos duros. No tiene que evitar crear subprocesos a toda costa, pero crear cientos de ellos por segundo no es una jugada inteligente. En la mayoría de los casos, si su diseño requiere muchos subprocesos, debe usar un grupo de subprocesos de tamaño limitado.
Hay dos tipos de hilos:
Hilos apropiados : estas son abstracciones alrededor de las instalaciones de enhebrado del sistema operativo subyacente. La creación de subprocesos es, por lo tanto, tan costosa como la del sistema; siempre hay una sobrecarga.
Hilos "verdes" : creados y programados por la JVM, estos son más baratos, pero no se produce un paralelismo apropiado. Estos se comportan como hilos, pero se ejecutan dentro del hilo JVM en el sistema operativo. No se usan a menudo, que yo sepa.
El factor más importante que puedo pensar en la sobrecarga de creación de subprocesos es el tamaño de pila que ha definido para sus subprocesos. El tamaño de pila de subprocesos se puede pasar como un parámetro cuando se ejecuta la máquina virtual.
Aparte de eso, la creación de subprocesos es principalmente dependiente de OS, e incluso depende de la implementación de VM.
Ahora, permítanme señalar algo: crear subprocesos es costoso si planea disparar 2000 hilos por segundo, cada segundo de su tiempo de ejecución. La JVM no está diseñada para manejar eso . Si tienes un par de trabajadores estables que no serán despedidos y asesinados una y otra vez, relájate.
La creación de Threads
requiere asignar una buena cantidad de memoria, ya que tiene que crear no una, sino dos pilas nuevas (una para el código Java y otra para el código nativo). El uso de Executors / Executors subprocesos puede evitar la sobrecarga, mediante la reutilización de subprocesos para tareas múltiples para el Executor .
La creación de subprocesos Java es costosa porque implica un poco de trabajo:
- Se debe asignar e inicializar un gran bloque de memoria para la pila de subprocesos.
- Las llamadas al sistema deben realizarse para crear / registrar el hilo nativo con el sistema operativo host.
- Los descriptores deben crearse, inicializarse y agregarse a las estructuras de datos internas de JVM.
También es costoso en el sentido de que el hilo ata recursos siempre que esté vivo; por ejemplo, la pila de subprocesos, cualquier objeto al que se pueda acceder desde la pila, los descriptores de subprocesos de JVM y los descriptores de subprocesos nativos del sistema operativo.
Los costos de todas estas cosas son específicos de la plataforma, pero no son baratos en ninguna plataforma Java que haya conocido.
Una búsqueda en Google me encontró un viejo punto de referencia que informa una tasa de creación de subprocesos de ~ 4000 por segundo en Sun Java 1.4.1 en un procesador doble vintage 2002 Xeon con Linux vintage 2002. Una plataforma más moderna dará mejores números ... y no puedo comentar sobre la metodología ... pero al menos da una idea general de cuán costosa es la creación de hilos.
La evaluación comparativa de Peter Lawrey indica que la creación de subprocesos es significativamente más rápida actualmente en términos absolutos, pero no está claro cuánto de esto se debe a mejoras en Java y / o el sistema operativo ... o velocidades de procesador más rápidas. Pero sus números aún indican una mejora de más de 150 veces si usa un grupo de hilos en lugar de crear / comenzar un nuevo hilo cada vez. (Y señala que todo esto es relativo ...)
(Lo anterior asume "hilos nativos" en lugar de "hilos verdes", pero las máquinas virtuales modernas utilizan hilos nativos por razones de rendimiento. Los hilos verdes son posiblemente más baratos de crear, pero pagas en otras áreas).
He hecho un poco de excavación para ver cómo se asigna realmente la pila de un hilo Java. En el caso de OpenJDK 6 en Linux, la pila de subprocesos se asigna mediante la llamada a pthread_create
que crea el hilo nativo. (La JVM no pasa pthread_create
una pila preasignada).
Luego, dentro de pthread_create
la pila se asigna mediante una llamada a mmap
siguiente manera:
mmap(0, attr.__stacksize,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
Según man mmap
, el indicador MAP_ANONYMOUS
hace que la memoria se inicialice a cero.
Por lo tanto, aunque no sea esencial que las nuevas pilas de hilos de Java se pongan a cero (según la especificación de JVM), en la práctica (al menos con OpenJDK 6 en Linux) se ponen a cero.
Obviamente, el quid de la cuestión es qué significa "caro".
Un hilo necesita crear una pila e inicializar la pila en función del método de ejecución.
Necesita configurar estructuras de estado de control, es decir, en qué estado está ejecutable, esperar, etc.
Probablemente hay una buena cantidad de sincronización alrededor de configurar estas cosas.
Otros han discutido de dónde vienen los costos de enhebrar. Esta respuesta cubre por qué la creación de un hilo no es tan cara en comparación con muchas operaciones, pero es relativamente costosa en comparación con las alternativas de ejecución de tareas, que son relativamente menos costosas.
La alternativa más obvia para ejecutar una tarea en otro hilo es ejecutar la tarea en el mismo hilo. Esto es difícil de entender para aquellos que asumen que más hilos siempre son mejores. La lógica es que si la sobrecarga de agregar la tarea a otro subproceso es mayor que el tiempo que guarda, puede ser más rápido realizar la tarea en el subproceso actual.
Otra alternativa es usar un grupo de subprocesos. Un grupo de subprocesos puede ser más eficiente por dos razones. 1) reutiliza los hilos ya creados. 2) puede sintonizar / controlar la cantidad de hilos para garantizar un rendimiento óptimo.
El siguiente programa imprime ...
Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us
Esta es una prueba para una tarea trivial que expone la sobrecarga de cada opción de subprocesamiento. (Esta tarea de prueba es el tipo de tarea que realmente se realiza mejor en el hilo actual).
final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
@Override
public void run() {
queue.add(1);
}
};
for (int t = 0; t < 3; t++) {
{
long start = System.nanoTime();
int runs = 20000;
for (int i = 0; i < runs; i++)
new Thread(task).start();
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
}
{
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService es = Executors.newFixedThreadPool(threads);
long start = System.nanoTime();
int runs = 200000;
for (int i = 0; i < runs; i++)
es.execute(task);
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
es.shutdown();
}
{
long start = System.nanoTime();
int runs = 200000;
for (int i = 0; i < runs; i++)
task.run();
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
}
}
}
Como puede ver, crear un nuevo hilo solo cuesta ~ 70 μs. Esto podría considerarse trivial en muchos casos de uso, si no en la mayoría. Relativamente hablando, es más costoso que las alternativas y para algunas situaciones un grupo de subprocesos o no usar subprocesos es una mejor solución.