Si bien los hilos pueden acelerar la ejecución del código, ¿son realmente necesarios? ¿Se puede hacer cada pieza de código usando un solo hilo o hay algo que existe que solo se puede lograr usando múltiples hilos?
Si bien los hilos pueden acelerar la ejecución del código, ¿son realmente necesarios? ¿Se puede hacer cada pieza de código usando un solo hilo o hay algo que existe que solo se puede lograr usando múltiples hilos?
Respuestas:
En primer lugar, los hilos no pueden acelerar la ejecución del código. No hacen que la computadora funcione más rápido. Todo lo que pueden hacer es aumentar la eficiencia de la computadora mediante el uso de tiempo que de otra manera se desperdiciaría. En ciertos tipos de procesamiento, esta optimización puede aumentar la eficiencia y disminuir el tiempo de ejecución.
La respuesta simple es sí. Puede escribir cualquier código que se ejecute en un solo hilo. Prueba: un sistema de procesador único solo puede ejecutar instrucciones linealmente. El sistema operativo realiza varias líneas de ejecución, procesa interrupciones, guarda el estado del subproceso actual e inicia otra.
La respuesta compleja es ... ¡más compleja! La razón por la que los programas multiproceso a menudo pueden ser más eficientes que los lineales se debe a un "problema" de hardware. La CPU puede ejecutar cálculos más rápidamente que la memoria y el almacenamiento duro IO. Entonces, una instrucción "agregar", por ejemplo, se ejecuta mucho más rápido que una "búsqueda". Las memorias caché y la búsqueda de instrucciones de programas dedicados (no estoy seguro del término exacto aquí) pueden combatir esto hasta cierto punto, pero el problema de la velocidad persiste.
Subprocesar es una forma de combatir este desajuste mediante el uso de la CPU para las instrucciones vinculadas a la CPU mientras se completan las instrucciones IO. Un plan típico de ejecución de subprocesos probablemente sería: Obtener datos, procesar datos, escribir datos. Suponga que la búsqueda y la escritura toman 3 ciclos y el procesamiento toma uno, con fines ilustrativos. ¿Puedes ver que mientras la computadora lee o escribe, no hace nada durante 2 ciclos cada uno? ¡Claramente está siendo flojo, y necesitamos descifrar nuestro látigo de optimización!
Podemos reescribir el proceso usando subprocesos para usar este tiempo perdido:
Y así. Obviamente, este es un ejemplo un tanto artificial, pero puede ver cómo esta técnica puede utilizar el tiempo que de lo contrario se gastaría esperando IO.
Tenga en cuenta que el enhebrado como se muestra arriba solo puede aumentar la eficiencia en procesos fuertemente vinculados a E / S. Si un programa está calculando principalmente cosas, no habrá muchos "agujeros" en los que podríamos trabajar más. Además, hay una sobrecarga de varias instrucciones al cambiar entre hilos. Si ejecuta demasiados subprocesos, la CPU pasará la mayor parte del tiempo cambiando y no trabajando mucho en el problema. Esto se llama paliza .
Todo eso está muy bien para un procesador de un solo núcleo, pero la mayoría de los procesadores modernos tienen dos o más núcleos. Los subprocesos siguen teniendo el mismo propósito: maximizar el uso de la CPU, pero esta vez tenemos la capacidad de ejecutar dos instrucciones separadas al mismo tiempo. Esto puede disminuir el tiempo de ejecución en función de la cantidad de núcleos disponibles, porque la computadora es realmente multitarea, no cambia de contexto.
Con múltiples núcleos, los hilos proporcionan un método para dividir el trabajo entre los dos núcleos. Sin embargo, lo anterior aún se aplica a cada núcleo individual; Un programa que ejecuta una eficiencia máxima con dos subprocesos en un núcleo probablemente se ejecutará con la máxima eficiencia con aproximadamente cuatro subprocesos en dos núcleos. (La eficiencia se mide aquí mediante ejecuciones mínimas de instrucciones NOP).
Los problemas con la ejecución de subprocesos en múltiples núcleos (a diferencia de un solo núcleo) generalmente son resueltos por el hardware. La CPU se asegurará de bloquear las ubicaciones de memoria apropiadas antes de leer / escribir en ella. (He leído que usa un bit de indicador especial en la memoria para esto, pero esto se puede lograr de varias maneras). Como programador con lenguajes de nivel superior, no tiene que preocuparse por nada más en dos núcleos mientras tendría que ver con uno.
TL; DR: los subprocesos pueden dividir el trabajo para permitir que la computadora procese varias tareas de forma asincrónica. Esto permite que la computadora funcione con la máxima eficiencia al utilizar todo el tiempo de procesamiento disponible, en lugar de bloquearse cuando un proceso está esperando un recurso.
¿Qué pueden hacer varios hilos que no puede hacer un solo hilo?
Nada.
Bosquejo de prueba simple:
Sin embargo, tenga en cuenta que hay una gran suposición oculta allí: a saber, que el lenguaje utilizado dentro del hilo único es Turing completo.
Entonces, la pregunta más interesante sería: "¿Puede agregar solo subprocesos múltiples a un lenguaje que no sea Turing completo hacer que Turing sea completo?" Y creo que la respuesta es "Sí".
Tomemos los lenguajes funcionales totales. [Para aquellos que no están familiarizados: al igual que la programación funcional es programar con funciones, la programación funcional total es programar con funciones totales.]
Total de los lenguajes funcionales, obviamente, no son Turing-completo: no se puede escribir un bucle infinito en un TFPL (de hecho, eso es más o menos la definición de "total"), pero se puede en una máquina de Turing, ergo existe al menos un programa que no se puede escribir en un TFPL pero sí en un UTM, por lo tanto, los TFPL son menos potentes desde el punto de vista computacional que los UTM.
Sin embargo, tan pronto como agregue subprocesos a un TFPL, obtendrá bucles infinitos: solo haga cada iteración del bucle en un nuevo subproceso. Cada subproceso individual siempre devuelve un resultado, por lo tanto, es Total, pero cada subproceso también genera un nuevo subproceso que ejecuta la siguiente iteración, hasta el infinito.
Yo creo que este lenguaje sería Turing completo.
Como mínimo, responde a la pregunta original:
¿Qué pueden hacer varios hilos que no puede hacer un solo hilo?
Si tiene un lenguaje que no puede hacer bucles infinitos, entonces el subprocesamiento múltiple le permite hacer bucles infinitos.
Tenga en cuenta, por supuesto, que generar un hilo es un efecto secundario y, por lo tanto, nuestro lenguaje extendido no solo ya no es Total, sino que ya ni siquiera es Funcional.
En teoría, todo lo que hace un programa multiproceso también se puede hacer con un programa de subproceso único, solo que más lento.
En la práctica, la diferencia de velocidad puede ser tan grande que no hay forma de utilizar un programa de subproceso único para la tarea. Por ejemplo, si tiene un trabajo de procesamiento de datos por lotes ejecutándose todas las noches, y tarda más de 24 horas en terminar en un solo hilo, no tiene otra opción que hacerlo multiproceso. (En la práctica, el umbral es probablemente aún menor: a menudo, dichas tareas de actualización deben finalizar antes de la mañana, antes de que los usuarios comiencen a usar el sistema nuevamente. Además, otras tareas pueden depender de ellas, que también deben finalizar durante la misma noche. el tiempo de ejecución disponible puede ser tan bajo como unas pocas horas / minutos).
Hacer trabajo informático en múltiples hilos es una forma de procesamiento distribuido; Usted está distribuyendo el trabajo en múltiples hilos. Otro ejemplo de procesamiento distribuido (usando múltiples computadoras en lugar de múltiples hilos) es el protector de pantalla SETI: procesar tantos datos de medición en un solo procesador tomaría mucho tiempo y los investigadores preferirían ver los resultados antes de la jubilación ;-) Sin embargo, no tienen el presupuesto para alquilar una supercomputadora durante tanto tiempo, por lo que distribuyen el trabajo en millones de PC domésticas, para que sea barato.
Aunque los hilos parecen ser un pequeño paso de la computación secuencial, de hecho, representan un gran paso. Descartan las propiedades más esenciales y atractivas de la computación secuencial: comprensibilidad, previsibilidad y determinismo. Los subprocesos, como modelo de cálculo, son tremendamente no deterministas, y el trabajo del programador se convierte en el de podar ese no determinismo.
- El problema con hilos (www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf).
Si bien hay algunas ventajas de rendimiento que se pueden obtener al usar hilos en los que puede distribuir el trabajo en múltiples núcleos, a menudo tienen un excelente precio.
Una de las desventajas del uso de subprocesos no mencionados aún aquí es la pérdida de compartimentación de recursos que se obtiene con espacios de proceso de subproceso único. Por ejemplo, supongamos que se encuentra con el caso de una segfault. Es posible, en algunos casos, recuperarse de esto en una aplicación multiproceso, ya que simplemente deja morir al niño con fallas y reaparece uno nuevo. Este es el caso en el backend prefork de Apache. Cuando una instancia de httpd se arruina, el peor de los casos es que la solicitud HTTP en particular puede descartarse para ese proceso, pero Apache genera un nuevo hijo y, a menudo, la solicitud solo se reenvía y se atiende. El resultado final es que Apache en su conjunto no se elimina con el hilo defectuoso.
Otra consideración en este escenario son las pérdidas de memoria. Hay algunos casos en los que puede manejar con gracia un bloqueo de subproceso (en UNIX, es posible recuperarse de algunas señales específicas, incluso segfault / fpviolation), pero incluso en ese caso, puede haber perdido toda la memoria asignada por ese subproceso (malloc, nuevo, etc.). Por lo tanto, aunque el proceso puede continuar, pierde cada vez más memoria con el tiempo con cada falla / recuperación. Una vez más, hay algunas formas de minimizar esto, como el uso de agrupaciones de memoria de Apache. Pero esto todavía no protege contra la memoria que podría haber sido asignada por librerías de terceros que el hilo podría haber estado usando.
Y, como han señalado algunas personas, comprender las primitivas de sincronización es quizás lo más difícil de entender. Este problema en sí mismo, solo obtener la lógica general correcta para todo su código, puede ser un gran dolor de cabeza. Los puntos muertos misteriosos son propensos a suceder en los momentos más extraños, y a veces ni siquiera hasta que su programa se haya ejecutado en producción, lo que hace que la depuración sea aún más difícil. Agregue a esto el hecho de que las primitivas de sincronización a menudo varían ampliamente con la plataforma (Windows vs. POSIX), y la depuración a menudo puede ser más difícil, así como la posibilidad de condiciones de carrera en cualquier momento (inicio / inicialización, tiempo de ejecución y apagado), La programación con hilos realmente tiene poca piedad para los principiantes. E incluso para expertos, Todavía hay poca misericordia solo porque el conocimiento del enhebrado en sí no minimiza la complejidad en general. Cada línea de código enhebrado a veces parece agravar exponencialmente la complejidad general del programa y también aumenta la probabilidad de que surja un punto muerto oculto o una extraña condición de carrera en cualquier momento. También puede ser muy difícil escribir casos de prueba para descubrir estas cosas.
Es por eso que algunos proyectos como Apache y PostgreSQL están en su mayor parte basados en procesos. PostgreSQL ejecuta cada subproceso de fondo en un proceso separado. Por supuesto, esto todavía no alivia el problema de la sincronización y las condiciones de carrera, pero agrega bastante protección y de alguna manera simplifica las cosas.
Múltiples procesos que ejecutan cada uno un solo subproceso de ejecución pueden ser mucho mejores que múltiples subprocesos que se ejecutan en un solo proceso. Y con la llegada de gran parte del nuevo código de igual a igual como AMQP (RabbitMQ, Qpid, etc.) y ZeroMQ, es mucho más fácil dividir subprocesos en diferentes espacios de proceso e incluso máquinas y redes, lo que simplifica enormemente las cosas. Pero aún así, no es una bala de plata. Todavía hay complejidad con la que lidiar. Simplemente mueve algunas de sus variables del espacio de procesos a la red.
La conclusión es que la decisión de ingresar al dominio de hilos no es ligera. Una vez que pisas ese territorio, casi instantáneamente todo se vuelve más complejo y nuevas razas de problemas entran en tu vida. Puede ser divertido y genial, pero es como la energía nuclear: cuando las cosas van mal, pueden ir mal y rápido. Recuerdo haber tomado una clase de entrenamiento en criticidad hace muchos años y mostraron fotos de algunos científicos en Los Alamos que jugaban con plutonio en los laboratorios de la Segunda Guerra Mundial. Muchos tomaron pocas o ninguna precaución contra el evento de una exposición, y en un abrir y cerrar de ojos, en un solo destello brillante e indoloro, todo habría terminado para ellos. Días después estaban muertos. Richard Feynman más tarde se refirió a esto como " cosquillas en la cola del dragón"."Así es como puede ser jugar con hilos (al menos para mí de todos modos). Parece bastante inocuo al principio, y cuando te muerden, te rascas la cabeza por la rapidez con la que las cosas se pusieron mal. Pero al menos los hilos ganaron No te mato.
En primer lugar, una aplicación de un solo subproceso nunca aprovechará una CPU de varios núcleos o un subprocesamiento múltiple. Pero incluso en un solo núcleo, la CPU de un solo subproceso que realiza subprocesos múltiples tiene ventajas.
Considera la alternativa y si eso te hace feliz. Suponga que tiene varias tareas que deben ejecutarse simultáneamente. Por ejemplo, debe seguir comunicándose con dos sistemas diferentes. ¿Cómo se hace esto sin subprocesos múltiples? Probablemente crearía su propio planificador y lo dejaría llamar a las diferentes tareas que deben realizarse. Esto significa que necesita dividir sus tareas en partes. Probablemente necesite cumplir con algunas restricciones en tiempo real, debe asegurarse de que sus piezas no ocupen demasiado tiempo. De lo contrario, el temporizador caducará en otras tareas. Esto hace que dividir una tarea sea más difícil. Cuantas más tareas necesite administrar, más división tendrá que hacer y más complejo será su programador para cumplir con todas las restricciones.
Cuando tienes múltiples hilos, la vida puede ser más fácil. Un planificador preventivo puede detener un subproceso en cualquier momento, mantener su estado y reiniciar (iniciar) otro. Se reiniciará cuando su hilo llegue su turno. Ventajas: la complejidad de escribir un programador ya se ha hecho por usted y no tiene que dividir sus tareas. Además, el planificador es capaz de administrar procesos / subprocesos que usted mismo ni siquiera conoce. Y también, cuando un hilo no necesita hacer nada (está esperando algún evento) no tomará ciclos de CPU. Esto no es tan fácil de lograr cuando está creando su programador de subproceso único. (Dormir algo no es tan difícil, pero ¿cómo se despierta?)
La desventaja del desarrollo de subprocesos múltiples es que debe comprender los problemas de concurrencia, las estrategias de bloqueo, etc. Desarrollar código multiproceso sin errores puede ser bastante difícil. Y la depuración puede ser aún más difícil.
¿Existe algo que solo pueda lograrse mediante el uso de múltiples hilos?
Si. No puede ejecutar código en múltiples CPU o núcleos de CPU con un solo hilo.
Sin múltiples CPU / núcleos, los subprocesos aún pueden simplificar el código que se ejecuta conceptualmente en paralelo, como el manejo del cliente en un servidor, pero podría hacer lo mismo sin subprocesos.
Los hilos no son solo sobre velocidad sino también sobre concurrencia.
Si no tiene una aplicación por lotes como sugirió @Peter, sino un kit de herramientas GUI como WPF, ¿cómo podría interactuar con los usuarios y la lógica empresarial con solo un hilo?
Además, suponga que está creando un servidor web. ¿Cómo serviría a más de un usuario simultáneamente con un solo hilo (suponiendo que no haya otros procesos)?
Hay muchos escenarios en los que un solo hilo simple no es suficiente. Es por eso que se están produciendo avances recientes como el procesador Intel MIC con más de 50 núcleos y cientos de hilos.
Sí, la programación paralela y concurrente es difícil. Pero necesario.
Multi-Threading puede permitir que la interfaz GUI siga respondiendo durante operaciones de procesamiento prolongadas. Sin subprocesos múltiples, el usuario estaría atrapado viendo un formulario bloqueado mientras se ejecuta un proceso largo.
El código de subprocesos múltiples puede bloquear la lógica del programa y acceder a datos obsoletos de una manera que los subprocesos individuales no pueden.
Los subprocesos pueden tomar un error oscuro de algo que un programador promedio puede depurar y moverlo al reino donde se cuentan historias de la suerte necesaria para atrapar el mismo error con los pantalones caídos cuando un programador de alerta estaba mirando solo el momento justo.
las aplicaciones que se ocupan del bloqueo de E / S que también deben permanecer receptivas a otras entradas (la GUI u otras conexiones) no se pueden hacer de una sola hebra
La adición de métodos de verificación en la biblioteca de IO para ver cuánto se puede leer sin bloquear puede ayudar a esto, pero no muchas bibliotecas ofrecen garantías completas sobre esto
Muchas buenas respuestas, pero no estoy seguro de ninguna frase como lo haría, tal vez esto ofrece una forma diferente de verlo:
Los subprocesos son solo una simplificación de programación como Objetos o Actores o para bucles (Sí, cualquier cosa que implemente con bucles puede implementar con if / goto).
Sin hilos simplemente implementas un motor de estado. He tenido que hacer esto muchas veces (la primera vez que lo hice nunca había oído hablar de él, solo hice una gran declaración de cambio controlada por una variable "Estado"). Las máquinas de estado siguen siendo bastante comunes, pero pueden ser molestas. Con hilos, desaparece una gran parte de la caldera.
También hacen que sea más fácil para un lenguaje dividir su ejecución en tiempo de ejecución en fragmentos compatibles con múltiples CPU (también lo hacen los actores, creo).
Java proporciona subprocesos "verdes" en sistemas donde el sistema operativo no proporciona NINGÚN soporte de subprocesos. En este caso, es más fácil ver que claramente no son más que una abstracción de programación.
Los sistemas operativos utilizan el concepto de división de tiempo en el que cada subproceso obtiene su tiempo de ejecución y luego se adelanta. Un enfoque como ese puede reemplazar el subproceso tal como está ahora, pero escribir sus propios programadores en cada aplicación sería excesivo. Además, tendría que trabajar con dispositivos de E / S, etc. Y requeriría un poco de soporte del lado del hardware, para que pueda disparar interrupciones para que su programador se ejecute. Básicamente estarías escribiendo un nuevo sistema operativo cada vez.
En general, el subproceso puede mejorar el rendimiento en los casos en que los subprocesos esperan E / S o están inactivos. También le permite crear interfaces que responden y permite detener procesos, mientras realiza tareas largas. Y también, el enhebrado mejora las cosas en las verdaderas CPU multinúcleo.
Primero, los hilos pueden hacer dos o más cosas al mismo tiempo (si tiene más de un núcleo). Si bien también puede hacer esto con múltiples procesos, algunas tareas simplemente no se distribuyen entre múltiples procesos muy bien.
Además, algunas tareas tienen espacios que no puede evitar fácilmente. Por ejemplo, es difícil leer datos de un archivo en el disco y también hacer que su proceso haga otra cosa al mismo tiempo. Si su tarea requiere necesariamente una gran cantidad de datos de lectura del disco, su proceso pasará mucho tiempo esperando el disco sin importar lo que haga.
En segundo lugar, los subprocesos pueden permitirle evitar tener que optimizar grandes cantidades de su código que no es crítico para el rendimiento. Si solo tiene un hilo único, cada pieza de código es crítica para el rendimiento. Si se bloquea, está hundido: ninguna tarea que se realizaría con ese proceso puede avanzar. Con los subprocesos, un bloque solo afectará a ese subproceso y otros subprocesos pueden aparecer y trabajar en tareas que ese proceso debe realizar.
Un buen ejemplo es el código de manejo de errores ejecutado con poca frecuencia. Digamos que una tarea encuentra un error muy infrecuente y el código para manejar ese error necesita paginarse en la memoria. Si el disco está ocupado y el proceso tiene un solo subproceso, no se puede avanzar hasta que el código para manejar ese error se pueda cargar en la memoria. Esto puede causar una respuesta explosiva.
Otro ejemplo es si rara vez tiene que hacer una búsqueda en la base de datos. Si espera a que la base de datos responda, su código tendrá un gran retraso. Pero no desea tomarse la molestia de hacer que todo este código sea asíncrono porque es muy raro que necesite hacer estas búsquedas. Con un hilo para hacer este trabajo, obtienes lo mejor de ambos mundos. Un hilo para hacer este trabajo hace que no sea crítico para el rendimiento como debería ser.