En informática , un hilo de ejecución es la secuencia más pequeña de instrucciones programadas que puede gestionar de forma independiente un programador , que normalmente forma parte del sistema operativo . [1] En muchos casos, un hilo es un componente de un proceso .
Los múltiples subprocesos de un proceso determinado pueden ejecutarse simultáneamente (mediante capacidades de subprocesamiento múltiple), compartiendo recursos como la memoria , mientras que los diferentes procesos no comparten estos recursos. En particular, los subprocesos de un proceso comparten su código ejecutable y los valores de sus variables asignadas dinámicamente y las variables globales no locales del subproceso en un momento dado.
La implementación de subprocesos y procesos difiere entre sistemas operativos. [2] [ página necesaria ]
Los subprocesos aparecieron por primera vez bajo el nombre de "tareas" en el sistema operativo de procesamiento por lotes de IBM, OS/360, en 1967. Proporcionaba a los usuarios tres configuraciones disponibles del sistema de control OS/360, de las cuales la multiprogramación con un número variable de tareas (MVT) era una. Saltzer (1966) atribuye a Victor A. Vyssotsky el término "subproceso". [3]
El uso de subprocesos en aplicaciones de software se volvió más común a principios de la década de 2000, cuando las CPU comenzaron a utilizar múltiples núcleos. Las aplicaciones que deseaban aprovechar los múltiples núcleos para obtener ventajas de rendimiento debían emplear la concurrencia para utilizar los múltiples núcleos. [4]
La programación se puede realizar a nivel de núcleo o de usuario, y la multitarea se puede realizar de forma preventiva o cooperativa . Esto genera una variedad de conceptos relacionados.
A nivel de núcleo, un proceso contiene uno o más hilos de núcleo , que comparten los recursos del proceso, como la memoria y los controladores de archivos: un proceso es una unidad de recursos, mientras que un hilo es una unidad de programación y ejecución. La programación del núcleo generalmente se realiza de manera uniforme de manera preventiva o, con menos frecuencia, de manera cooperativa. A nivel de usuario, un proceso como un sistema en tiempo de ejecución puede programar múltiples hilos de ejecución. Si estos no comparten datos, como en Erlang, generalmente se denominan procesos de manera análoga, [5] mientras que si comparten datos, generalmente se denominan hilos (de usuario) , particularmente si se programan de manera preventiva. Los hilos de usuario programados de manera cooperativa se conocen como fibras ; diferentes procesos pueden programar hilos de usuario de manera diferente. Los hilos de usuario pueden ser ejecutados por hilos de núcleo de varias maneras (uno a uno, muchos a uno, muchos a muchos). El término " proceso liviano " se refiere de diversas maneras a los hilos de usuario o a los mecanismos del núcleo para programar hilos de usuario en hilos de núcleo.
Un proceso es una unidad "pesada" de la programación del núcleo, ya que crear, destruir y cambiar procesos es relativamente costoso. Los procesos poseen recursos asignados por el sistema operativo. Los recursos incluyen memoria (tanto para código como para datos), identificadores de archivos , sockets, identificadores de dispositivos, ventanas y un bloque de control de procesos . Los procesos están aislados por aislamiento de procesos y no comparten espacios de direcciones o recursos de archivos excepto a través de métodos explícitos como heredar identificadores de archivos o segmentos de memoria compartida, o mapear el mismo archivo de manera compartida - ver comunicación entre procesos . Crear o destruir un proceso es relativamente costoso, ya que los recursos deben adquirirse o liberarse. Los procesos suelen ser multitarea preventivas, y el cambio de proceso es relativamente costoso, más allá del costo básico del cambio de contexto , debido a problemas como el vaciado de caché (en particular, el cambio de proceso cambia el direccionamiento de memoria virtual, causando invalidación y, por lo tanto, vaciado de un búfer de traducción lateral (TLB) sin etiquetar, especialmente en x86).
Un hilo de kernel es una unidad "liviana" de programación de kernel. Existe al menos un hilo de kernel dentro de cada proceso. Si existen múltiples hilos de kernel dentro de un proceso, entonces comparten los mismos recursos de memoria y archivo. Los hilos de kernel son multitarea preventiva si el programador de procesos del sistema operativo es preventivo. Los hilos de kernel no poseen recursos excepto una pila , una copia de los registros que incluyen el contador de programa y el almacenamiento local de hilo (si lo hay), y por lo tanto son relativamente baratos de crear y destruir. El cambio de hilo también es relativamente barato: requiere un cambio de contexto (guardar y restaurar registros y puntero de pila), pero no cambia la memoria virtual y, por lo tanto, es compatible con la caché (dejando válida la TLB). El kernel puede asignar uno o más hilos de software a cada núcleo en una CPU (pudiendo asignarse a sí mismo múltiples hilos de software dependiendo de su soporte para multihilo), y puede intercambiar hilos que se bloquean. Sin embargo, los hilos de kernel tardan mucho más en intercambiarse que los hilos de usuario.
Los subprocesos a veces se implementan en bibliotecas de espacio de usuario , por lo que se denominan subprocesos de usuario . El núcleo no los conoce, por lo que se administran y programan en el espacio de usuario. Algunas implementaciones basan sus subprocesos de usuario en varios subprocesos del núcleo, para aprovechar las máquinas multiprocesador (modelo M:N). Los subprocesos de usuario implementados por máquinas virtuales también se denominan subprocesos verdes .
Como las implementaciones de subprocesos de usuario suelen realizarse completamente en el espacio de usuario, el cambio de contexto entre subprocesos de usuario dentro del mismo proceso es extremadamente eficiente porque no requiere ninguna interacción con el núcleo: se puede realizar un cambio de contexto guardando localmente los registros de CPU utilizados por el subproceso de usuario o la fibra que se está ejecutando actualmente y luego cargando los registros que requiere el subproceso de usuario o la fibra que se va a ejecutar. Dado que la programación se realiza en el espacio de usuario, la política de programación se puede adaptar más fácilmente a los requisitos de la carga de trabajo del programa.
Sin embargo, el uso de llamadas al sistema bloqueantes en subprocesos de usuario (en contraposición a subprocesos del núcleo) puede ser problemático. Si un subproceso de usuario o una fibra realiza una llamada al sistema que se bloquea, los demás subprocesos de usuario y fibras del proceso no pueden ejecutarse hasta que la llamada al sistema regrese. Un ejemplo típico de este problema es cuando se realiza una operación de E/S: la mayoría de los programas están escritos para realizar E/S de manera sincrónica. Cuando se inicia una operación de E/S, se realiza una llamada al sistema y no regresa hasta que se haya completado la operación de E/S. En el período intermedio, todo el proceso está "bloqueado" por el núcleo y no puede ejecutarse, lo que impide que otros subprocesos de usuario y fibras del mismo proceso se ejecuten.
Una solución común a este problema (usada, en particular, por muchas implementaciones de subprocesos verdes) es proporcionar una API de E/S que implemente una interfaz que bloquee el subproceso que realiza la llamada, en lugar de todo el proceso, mediante el uso interno de E/S no bloqueante y la programación de otro subproceso de usuario o fibra mientras la operación de E/S está en curso. Se pueden proporcionar soluciones similares para otras llamadas al sistema bloqueantes. Alternativamente, el programa se puede escribir para evitar el uso de E/S sincrónicas u otras llamadas al sistema bloqueantes (en particular, utilizando E/S no bloqueantes, incluidas las continuaciones lambda y/o primitivas async/ await [6] ).
Las fibras son una unidad de programación aún más ligera que se programa de forma cooperativa : una fibra en ejecución debe " ceder " explícitamente para permitir que se ejecute otra fibra, lo que hace que su implementación sea mucho más sencilla que los subprocesos del núcleo o del usuario. Una fibra se puede programar para que se ejecute en cualquier subproceso del mismo proceso. Esto permite que las aplicaciones obtengan mejoras de rendimiento al gestionar la programación por sí mismas, en lugar de depender del programador del núcleo (que puede no estar ajustado para la aplicación). Algunas implementaciones de investigación del modelo de programación paralela OpenMP implementan sus tareas a través de fibras. [7] [8] Estrechamente relacionadas con las fibras están las corrutinas , con la distinción de que las corrutinas son una construcción a nivel de lenguaje, mientras que las fibras son una construcción a nivel de sistema.
Los subprocesos se diferencian de los procesos multitarea tradicionales del sistema operativo en varios aspectos:
Se dice que sistemas como Windows NT y OS/2 tienen subprocesos baratos y procesos costosos ; en otros sistemas operativos no hay una diferencia tan grande excepto en el costo de un cambio de espacio de direcciones , que en algunas arquitecturas (especialmente x86 ) resulta en un vaciado del buffer de traducción lateral (TLB).
Las ventajas y desventajas de los subprocesos frente a los procesos incluyen:
Los sistemas operativos programan los subprocesos de forma preventiva o cooperativa . Los sistemas operativos multiusuario generalmente favorecen el multiproceso preventivo por su control más preciso sobre el tiempo de ejecución a través del cambio de contexto . Sin embargo, la programación preventiva puede cambiar de contexto los subprocesos en momentos no previstos por los programadores, lo que provoca convoyes de bloqueo , inversión de prioridad u otros efectos secundarios. Por el contrario, el multiproceso cooperativo se basa en que los subprocesos renuncien al control de la ejecución, lo que garantiza que los subprocesos se ejecuten hasta el final . Esto puede causar problemas si un subproceso multitarea cooperativo se bloquea al esperar un recurso o si deja sin recursos a otros subprocesos al no ceder el control de la ejecución durante un cálculo intensivo.
Hasta principios de la década de 2000, la mayoría de las computadoras de escritorio tenían solo una CPU de un solo núcleo, sin soporte para subprocesos de hardware , aunque los subprocesos aún se usaban en dichas computadoras porque el cambio entre subprocesos generalmente seguía siendo más rápido que los cambios de contexto de proceso completo . En 2002, Intel agregó soporte para subprocesamiento múltiple simultáneo al procesador Pentium 4 , bajo el nombre de hiperprocesamiento ; en 2005, presentaron el procesador Pentium D de doble núcleo y AMD presentó el procesador Athlon 64 X2 de doble núcleo .
Los sistemas con un solo procesador generalmente implementan el multihilo mediante la división en segmentos de tiempo : la unidad central de procesamiento (CPU) cambia entre diferentes subprocesos de software . Este cambio de contexto suele ocurrir con la suficiente frecuencia como para que los usuarios perciban que los subprocesos o las tareas se ejecutan en paralelo (en los sistemas operativos populares de servidores y escritorios, la máxima división de tiempo de un subproceso, cuando otros subprocesos están esperando, suele estar limitada a 100-200 ms). En un sistema multiprocesador o multinúcleo , varios subprocesos pueden ejecutarse en paralelo , y cada procesador o núcleo ejecuta un subproceso independiente simultáneamente; en un procesador o núcleo con subprocesos de hardware , los subprocesos de software independientes también pueden ejecutarse simultáneamente por subprocesos de hardware independientes.
Los subprocesos creados por el usuario en una correspondencia 1:1 con entidades programables en el núcleo [9] son la implementación de subprocesos más simple posible. OS/2 y Win32 usaron este enfoque desde el principio, mientras que en Linux la biblioteca GNU C implementa este enfoque (a través de NPTL o LinuxThreads más antiguos ). Este enfoque también lo usan Solaris , NetBSD , FreeBSD , macOS e iOS .
Un modelo M :1 implica que todos los hilos de nivel de aplicación se asignan a una entidad programada a nivel de núcleo; [9] el núcleo no tiene conocimiento de los hilos de aplicación. Con este enfoque, el cambio de contexto se puede realizar muy rápidamente y, además, se puede implementar incluso en núcleos simples que no admiten subprocesos. Sin embargo, una de las principales desventajas es que no puede beneficiarse de la aceleración de hardware en procesadores multiproceso o computadoras multiprocesador : nunca se programa más de un hilo al mismo tiempo. [9] Por ejemplo: si uno de los hilos necesita ejecutar una solicitud de E/S, todo el proceso se bloquea y no se puede utilizar la ventaja de los subprocesos. GNU Portable Threads utiliza subprocesos a nivel de usuario, al igual que State Threads .
M : N asigna una cantidad M de subprocesos de aplicación a una cantidad N de entidades del núcleo [9] o "procesadores virtuales". Se trata de un compromiso entre la creación de subprocesos a nivel del núcleo ("1:1") y a nivel del usuario (" N :1"). En general, los sistemas de subprocesos " M : N " son más complejos de implementar que los subprocesos del núcleo o del usuario, porque se requieren cambios tanto en el código del núcleo como en el del espacio del usuario [ se necesita una aclaración ] . En la implementación M:N, la biblioteca de subprocesos es responsable de programar los subprocesos del usuario en las entidades programables disponibles; esto hace que el cambio de contexto de los subprocesos sea muy rápido, ya que evita las llamadas del sistema. Sin embargo, esto aumenta la complejidad y la probabilidad de inversión de prioridad , así como de una programación subóptima sin una coordinación extensa (y costosa) entre el programador del espacio del usuario y el programador del núcleo.
SunOS 4.x implementó procesos livianos o LWPs. NetBSD 2.x+ y DragonFly BSD implementan LWPs como hilos del núcleo (modelo 1:1). SunOS 5.2 a SunOS 5.8 así como NetBSD 2 a NetBSD 4 implementaron un modelo de dos niveles, multiplexando uno o más hilos de nivel de usuario en cada hilo del núcleo (modelo M:N). SunOS 5.9 y posteriores, así como NetBSD 5 eliminaron el soporte de hilos de usuario, volviendo a un modelo 1:1. [10] FreeBSD 5 implementó el modelo M:N. FreeBSD 6 admitía tanto 1:1 como M:N, los usuarios podían elegir cuál debería usarse con un programa determinado utilizando /etc/libmap.conf. A partir de FreeBSD 7, el 1:1 se convirtió en el predeterminado. FreeBSD 8 ya no admite el modelo M:N.
En programación informática , el procesamiento monohilo es el procesamiento de un comando a la vez. [11] En el análisis formal de la semántica de las variables y el estado del proceso, el término monohilo se puede utilizar de forma diferente para significar "retroceder dentro de un solo hilo", lo cual es común en la comunidad de programación funcional . [12]
El multihilo se encuentra principalmente en sistemas operativos multitarea. El multihilo es un modelo de programación y ejecución muy extendido que permite que existan varios subprocesos dentro del contexto de un proceso. Estos subprocesos comparten los recursos del proceso, pero pueden ejecutarse de forma independiente. El modelo de programación por subprocesos proporciona a los desarrolladores una abstracción útil de la ejecución concurrente. El multihilo también se puede aplicar a un proceso para permitir la ejecución en paralelo en un sistema multiprocesamiento .
Las bibliotecas de subprocesos múltiples suelen proporcionar una llamada a una función para crear un nuevo subproceso, que toma una función como parámetro. A continuación, se crea un subproceso simultáneo que comienza a ejecutar la función pasada y finaliza cuando la función retorna. Las bibliotecas de subprocesos también ofrecen funciones de sincronización de datos.
Los subprocesos del mismo proceso comparten el mismo espacio de direcciones. Esto permite que el código que se ejecuta simultáneamente se acople estrechamente e intercambie datos de manera conveniente sin la sobrecarga o la complejidad de un IPC . Sin embargo, cuando se comparten entre subprocesos, incluso las estructuras de datos simples se vuelven propensas a condiciones de carrera si requieren más de una instrucción de CPU para actualizarse: dos subprocesos pueden terminar intentando actualizar la estructura de datos al mismo tiempo y descubrir que cambia inesperadamente. Los errores causados por condiciones de carrera pueden ser muy difíciles de reproducir y aislar.
Para evitar esto, las interfaces de programación de aplicaciones (API) de subprocesos ofrecen primitivas de sincronización , como mutexes, para bloquear las estructuras de datos contra el acceso simultáneo. En sistemas monoprocesador, un subproceso que se ejecuta en un mutex bloqueado debe dormir y, por lo tanto, activar un cambio de contexto. En sistemas multiprocesador, el subproceso puede, en cambio, sondear el mutex en un spinlock . Ambos pueden minar el rendimiento y obligar a los procesadores en sistemas de multiprocesamiento simétrico (SMP) a competir por el bus de memoria, especialmente si la granularidad del bloqueo es demasiado fina.
Otras API de sincronización incluyen variables de condición , secciones críticas , semáforos y monitores .
Un patrón de programación popular que involucra subprocesos es el de los grupos de subprocesos , donde se crea una cantidad determinada de subprocesos al inicio que luego esperan a que se les asigne una tarea. Cuando llega una nueva tarea, se activa, completa la tarea y vuelve a esperar. Esto evita las funciones relativamente costosas de creación y destrucción de subprocesos para cada tarea realizada y quita la gestión de subprocesos de las manos del desarrollador de la aplicación y la deja en manos de una biblioteca o del sistema operativo que sea más adecuado para optimizar la gestión de subprocesos.
Las aplicaciones multiproceso tienen las siguientes ventajas frente a las de un solo subproceso:
Las aplicaciones multiproceso tienen las siguientes desventajas:
Muchos lenguajes de programación admiten subprocesos de alguna manera.
{{cite AV media}}
: CS1 maint: bot: estado de URL original desconocido ( enlace )