Logo Studenta

Bibliografia_cap4

¡Este material tiene más páginas!

Vista previa del material en texto

Cap 4
Hilos y Concurrencia
El modelo de proceso presentado en el Capítulo 3 suponía que un proceso era un programa en ejecución con sólo un hilo de control. Prácticamente todos los sistemas operativos modernos proporcionan características que permiten que un proceso contenga múltiples hilos de control. Hay que identificar oportunidades de paralelismo. Mediante el uso de hilos es cada vez más importante para los sistemas multinúcleos modernos que proporcionan múltiples CPU.
En este capítulo, presentamos muchos conceptos, así como desafíos, asociados con sistemas de computación multihilos, incluida una discusión sobre las API para las bibliotecas de hilos de Pthreads, Windows y Java. Además, exploramos varias características nuevas que abstraen el concepto de crear hilos, permitiendo desarrolladores para centrarse en identificar oportunidades para el paralelismo y dejar las funciones de lenguaje y los marcos de API gestionan los detalles de la creación de hilos y su gestión. Observamos una serie de cuestiones relacionadas con la programación multihilo y su efecto en el diseño de sistemas operativos. Finalmente exploramos cómo los sistemas operativos Windows y Linux admiten hilos en el nivel del kernel.
OBJETIVOS DEL CAPÍTULO
• Identificar los componentes básicos de un hilo, y contrastar los hilos y los procesos.
• Describir los principales beneficios y desafíos importantes del diseño de procesos multi-hilos.
• Ilustrar diferentes enfoques para el hilado implícito, incluidos los grupos de hilos, fork-join y Grand Central Dispatch.
• Describir cómo representan hilos los sistemas operativos Windows y Linux..
• Diseñar aplicaciones multihilos utilizando APIs de hilos: Pthreads, Java y Windows.
4.1.1 Motivación
La mayoría de las aplicaciones de software que se ejecutan en computadoras modernas y dispositivos móviles son multihilados. Una aplicación generalmente se implementa como un proceso separado con varios hilos de control.
Las aplicaciones también se pueden diseñar para aprovechar las capacidades de procesamiento en sistemas multinúcleos. Dichas aplicaciones pueden realizar varias tareas intensivas de CPU en paralelo a través de los múltiples núcleos.
A continuación destacamos algunos ejemplos de aplicaciones multihilo:
• Un procesador de texto puede tener un hilo para mostrar gráficos, otro hilo para responder a las pulsaciones de teclas del usuario, y un tercer hilo para revisar la ortografía y la revisión gramatical en segundo plano.
• Un navegador web puede tener un hilo para mostrar imágenes o texto mientras que otro hilo recupera datos de la red: En ciertas situaciones, se puede requerir una sola aplicación para realizar varias tareas similares Por ejemplo, un servidor web acepta solicitudes de clientes para páginas web, imágenes, sonido, etc. Un servidor web puede estar ocupado al tener varios (quizás miles de) clientes que acceden simultáneamente. Si el servidor web se ejecuta como un proceso tradicional de un solo hilo, sería capaz de atender solo a un cliente a la vez, y un cliente puede tener que esperar mucho tiempo para que su solicitud sea atendida.
Figura 4.1 Arquitectura de servidor multihilo.
Una solución es hacer que el servidor se ejecute como un proceso único que acepta peticiones. Cuando el servidor recibe una solicitud, crea un proceso separado para atender esa solicitud. De hecho, este método se usaba mucho (la creación de procesos era común antes de que los hilos se vuelvan populares). La creación de procesos lleva mucho tiempo y hacía uso intensivo de los recursos. Sin embargo, Si el nuevo proceso realizara las mismas tareas que
el proceso existente, ¿por qué incurrir en todos esos gastos generales? Generalmente es más eficiente usar un proceso que contiene múltiples hilos. Si el proceso del servidor web es multihilo, el servidor creará un hilo separado para atender a cada petición de cliente. Cuando se realiza una solicitud, en lugar de crear otro proceso, el servidor crea un nuevo hilo para atender la solicitud y reanuda la escucha de requerimientos adicionales. Esto se ilustra en la Figura 4.1.
Visión general
Un hilo es una unidad básica de utilización de la CPU; comprende una:
· ID de hilo, 
· un contador de programa (PC), 
· un conjunto de registros y 
· una pila. 
Se comparte con otros hilos pertenecientes al mismo proceso:
· su sección de código, 
· sección de datos y 
· otros recursos del sistema operativo, como archivos abiertos y señales. 
Un proceso tradicional tiene un solo hilo de control. Si un proceso tiene múltiples hilos de control, puede realizar más de una tarea a la vez. La figura 4.2 ilustra la diferencia entre un proceso tradicional con un solo hilo y un proceso multihilos.
Figura 4.2 Proceso de único hilo y Proceso de múltiples hilos
La mayoría de los kernels de los sistemas operativos también suelen ser multihilos. Como un ejemplo, durante el tiempo de arranque del sistema en sistemas Linux, varios hilos del kernel son creados. Cada hilo realiza una tarea específica, tales como el administrador de dispositivos, el gestor de memoria o el manejador de interrupciones. El comando ps -ef 
puede ser usado para mostrar los hilos del kernel en un sistema Linux en ejecución. Examinando la salida de este comando mostrará el hilo del kernel 
kthreadd (con pid = 2),
que sirve como padre de todos los otros hilos del kernel.
Muchas aplicaciones también pueden aprovechar múltiples hilos, incluidos en programas de ordenamiento o clasificación, árboles y algoritmos gráficos. Además, los programadores que deben resolver problemas de uso intensivo de CPU, como en minería de datos, gráficos y de inteligencia artificial puede aprovechar el poder de los sistemas multicores modernos al diseñar soluciones que se ejecutan en paralelo.
4.1.2 Beneficios
Los beneficios de la programación multihilo se pueden dividir en cuatro Categorías principales:
1. Capacidad de respuesta. Los múltiples hilos de una aplicación interactiva pueden permitir a un programa continuar ejecutándose incluso si parte de él está bloqueado o está ejecutando una operación prolongada, lo que aumenta la capacidad de respuesta al usuario. Esta cualidad es especialmente útil en el diseño de interfaces de usuario. Por ejemplo, considere lo que sucede cuando un usuario hace clic en un botón que da como resultado observar el rendimiento de una operación que consume mucho tiempo. Una aplicación de un solo hilo no respondería al usuario hasta que la operación hubiera sido terminado. Por el contrario, si la operación que lleva mucho tiempo se realiza en un hilo separado y asíncrono, la aplicación sigue respondiendo al usuario.
2. Compartir recursos. Los procesos pueden compartir recursos solo a través de técnicas como la memoria compartida y el paso de mensajes. Tales técnicas deben ser organizadas explícitamente por el programador. Sin embargo, los hilos comparten la memoria y los recursos del proceso al que pertenecen por defecto. El beneficio de compartir código y datos es que permite a una aplicación tener varios hilos diferentes de actividad dentro del mismo espacio de direcciones.
3. Economía. Como la creación de procesos consume mucha memoria y recursos, ya que los hilos comparten los recursos del proceso al que pertenecen, es más económico crear y cambiar hilos (cambio de contexto de hilos).
Medir empíricamente la diferencia en gastos generales puede ser difícil, pero en la creación general de hilos consume menos tiempo y memoria que la creación de procesos. Además, el cambio de contexto suele ser más rápido entre hilos que entre procesos.
4. Escalabilidad. Los beneficios del multihilo pueden ser aún mayores en una arquitectura multiprocesador, donde los hilos pueden ejecutarse en paralelo en diferentes núcleos de procesamiento. Un proceso de hilo único solo puede ejecutarse en un procesador, independientemente de cuántos estén disponibles. Exploramos este tema más adelante en la siguiente sección.
4.2 Concurrencia vs. Paralelismo
En un sistema con un solo núcleo de computación,la concurrencia significa simplemente que la ejecución de los hilos será intercalada en el tiempo (Figura 4.3), porque el núcleo de procesamiento es capaz de ejecutar sólo un hilo a la vez 
Figura 4.3 Ejecución concurrente en un sistema de un solo núcleo.
Sin embargo, en un sistema con múltiples núcleos, concurrencia significa que algunos hilos pueden ejecutarse en paralelo, porque el sistema puede asignar un hilo separado a cada núcleo (Figura 4.4).
Figura 4.4 Ejecución paralela en un sistema multinúcleo.
Observe la distinción entre concurrencia y paralelismo en esta discusión. Un sistema concurrente admite más de una tarea al permitir progresar a todas las tareas. Por el contrario, un sistema paralelo puede realizar más de una tarea simultáneamente. Por lo tanto, es posible tener concurrencia sin paralelismo. Antes del advenimiento de las arquitecturas multiprocesador y multinúcleo, en la mayoría de las computadoras, los sistemas tenían un solo procesador y se diseñaron planificadores de CPU para proporcionar la ilusión de paralelismo cambiando rápidamente entre procesos, permitiendo así que cada proceso progrese. Tales procesos se estaban ejecutando
concurrentemente, pero no en paralelo.
Programación multinúcleo
Anteriormente en el diseño de computadoras, en respuesta a la necesidad de más rendimiento computacional, los sistemas de CPU única evolucionaron en sistemas de CPU múltiple. Una tendencia posterior, pero similar, en el diseño del sistema es colocar múltiple núcleos de computación en un solo chip de procesamiento donde cada núcleo aparece al sistema operativo como una CPU separada (Sección 1.3.2). Nos referimos a sistemas como multinúcleo, y la programación multihilo proporciona un mecanismo para una mayor eficiencia uso de estos múltiples núcleos de computación y aprovechar o mejorar la concurrencia. Considere una aplicación con cuatro hilos. 
4.2.1 Desafíos de programación
La tendencia hacia sistemas multinúcleo continúa ejerciendo presión sobre los diseñadores de sistema y sobre los programadores de aplicaciones para hacer un mejor uso de los múltiples núcleos de computación. Los diseñadores de sistemas operativos deben escribir algoritmos de planificación que utilicen múltiples núcleos de procesamiento para permitir la ejecución paralela que se muestra en la figura 4.4. Para los programadores de aplicaciones, el desafío es modificar los programas existentes, así como diseñar nuevos programas que sean multihilo.
En general, cinco áreas presentan desafíos en la programación para sistemas multinúcleo:
1. Identificación de tareas. Esto implica examinar aplicaciones para encontrar áreas que se puede dividir en tareas separadas y concurrentes. Idealmente, las tareas son independientes entre sí y, por lo tanto, pueden ejecutarse en paralelo en núcleos individuales.
2. Balance. Al identificar tareas que pueden ejecutarse en paralelo, los programadores también deben asegurarse de que las tareas aporten al mismo trabajo. En algunos casos, una determinada tarea puede no aportar tanto valor al proceso general como otras tareas. Usar un núcleo de ejecución separado para ejecutar esa tarea puede que no valga la pena
3. División de datos. Así como las aplicaciones se dividen en tareas separadas, los datos accedidos y manipulados por las tareas deben dividirse para ejecutarse en núcleos separados.
4. Dependencia de datos. Los datos a los que acceden las tareas deben examinarse por las dependencias entre dos o más tareas. Cuando una tarea depende de los datos de otra, los programadores deben asegurarse de que la ejecución de las tareas se sincronicen para acomodar la dependencia de datos. Examinamos tales estrategias en el Capítulo 6.
5. Pruebas y depuración. Cuando un programa se ejecuta en paralelo en múltiples núcleos, es posible muchas rutas de ejecución diferentes. Las Pruebas y depuración de dichos programas concurrentes son inherentemente más difíciles que las pruebas y depuración de aplicaciones de hilo único.
Debido a estos desafíos, muchos desarrolladores de software argumentan que la llegada de los sistemas multinúcleo requerirán un enfoque completamente nuevo para diseñar sistemas de software en el futuro. (Del mismo modo, muchos docentes de computación creen que el desarrollo de software debe enseñarse con mayor énfasis en programación paralela.)
4.2.2 Tipos de paralelismo
En general, hay dos tipos de paralelismo: paralelismo de datos y paralelismo de tareas. El paralelismo de datos se centra en la distribución de subconjuntos de los mismos datos a través de múltiples núcleos de computación y realizando la misma operación en cada núcleo. Considere, por ejemplo, sumar el contenido de una matriz de tamaño N. En un sistema de un solo núcleo, un hilo simplemente sumaría los elementos [0]. . . [N - 1] Sin embargo, en un sistema de doble núcleo, el hilo A, que se ejecuta en el núcleo 0, podría sumar los elementos [0]. . . [N ∕ 2 - 1] mientras el hilo B, que se ejecuta en el núcleo 1, podría sumar los elementos [N ∕ 2]. . . [N - 1] Los dos hilos se ejecutarían en paralelo en núcleos de computación separados.
El paralelismo de tareas implica la distribución, no de datos, sino de tareas (hilos) a través de Múltiples núcleos de computación. Cada hilo está realizando una operación única. Diferentes hilos pueden estar operando con los mismos datos, o pueden estar operando con datos diferentes. Considere nuevamente nuestro ejemplo anterior. En contraste con esa situación, Un ejemplo de paralelismo de tareas podría involucrar dos hilos, cada uno de ellos ejecutando una operación aritmética única en la matriz de elementos. Los hilos nuevamente son cómputos en paralelo en núcleos de computación separados, pero cada uno realiza una operación única.
Básicamente, el paralelismo de datos implica la distribución de datos a través de múltiples núcleos, y el paralelismo de tareas implica la distribución de tareas a través de múltiples núcleos, como se muestra en la Figura 4.5. Sin embargo, el paralelismo de datos y tareas no son mutuamente excluyentes, y una aplicación de hecho puede usar un híbrido de estas dos estrategias.
Figura 4.5 Paralelismo de Datos y tareas.
LEY DE AMDAHL
La Ley de Amdahl es una fórmula que identifica posibles ganancias de rendimiento al agregar núcleos de computación adicionales a una aplicación que tiene tanto serial (no paralelo) y componentes paralelos. Si S es la parte de la aplicación que debe realizarse en serie en un sistema con N núcleos de procesamiento, la fórmula aparece de la siguiente manera, La ACELERACIÓN, será:
 
Como ejemplo, supongamos que tenemos una aplicación que es 75 por ciento paralela y 25 por ciento en serie. Si ejecutamos esta aplicación en un sistema con dos núcleos de procesamiento, podemos obtener una aceleración de 1.6 veces. Si agregamos dos núcleos adicionales (para un total de cuatro), la aceleración es 2.28 veces. A continuación hay un gráfico que ilustra la ley de Amdahl en escenarios diferentes.
Un hecho interesante sobre la Ley de Amdahl es que a medida que N se acerca a infinito, la aceleración converge a 1 ∕ S. Por ejemplo, si el 50 por ciento de una aplicación se realiza en serie, la velocidad máxima es 2.0 veces, independientemente del Número de núcleos de procesamiento que agreguemos. Este es el principio fundamental detrás de Ley de Amdahl: la parte en serie de una aplicación puede tener un efecto negativo en el rendimiento que ganamos al agregar núcleos de computación adicionales.
4.3 Modelos de hilos múltiples
Nuestra discusión hasta ahora ha tratado los hilos en un sentido genérico. Sin embargo, el apoyo para hilos se puede proporcionar a nivel de usuario, para hilos de usuario o para kernel, para hilos de kernel. Los hilos de usuario son compatibles por encima del kernel y se administran sin soporte de kernel, mientras que los hilos de kernel son compatibles y gestionados directamente por elsistema operativo. 
Prácticamente todos los sistemas operativos contemporáneos, incluidos Windows, Linux y macOS, admiten hilos del kernel. 
En definitiva, debe existir una relación entre los hilos de usuario y los hilos del kernel, como se ilustra en la Figura 4.6. En esta sección, nos fijamos en tres formas comunes de establecer tal relación: 
· modelo de muchos a uno, 
· modelo de uno a uno y 
· modelo de muchos a muchos.
Figura 4.6 Hilos de usuario y kernel.
4.3.1 Modelo muchos a uno
El modelo de muchos a uno (Figura 4.7) asigna muchos hilos de nivel de usuario a un hilo de kernel. La administración de hilos es realizada por la biblioteca de hilos en el espacio del usuario, entonces es eficiente (discutimos las bibliotecas de hilos en la Sección 4.4). 
Figura 4.7 Modelo de muchos a uno.
Sin embargo, todo el proceso se bloqueará si un hilo realiza una llamada al sistema que sea bloqueante. Además, porque sólo un hilo puede acceder al kernel a la vez, varios hilos no pueden ejecutarse en paralelo en sistemas multinúcleo. Hilos verdes (Green threads): una biblioteca de hilos disponible para los sistemas Solaris y adoptado en las primeras versiones de Java, se utilizaban modelos de muchos a uno. Sin embargo, muy pocos sistemas continúan usando el modelo debido a su incapacidad para aprovechar múltiples núcleos de procesamiento, cuya potencia aparece en la mayoría de los sistemas de computación.
4.3.2 Modelo uno a uno
El modelo uno a uno (Figura 4.8) asigna cada hilo de usuario a un hilo del kernel. Eso proporciona más concurrencia que el modelo de muchos a uno al permitir que otro hilo se ejecute cuando un hilo hace una llamada al sistema bloqueante. También permite múltiples hilos para ejecutarse en paralelo en multiprocesadores. El único inconveniente de este modelo es que crear un hilo de usuario requiere crear el correspondiente hilo del kernel, y una gran cantidad de hilos del kernel pueden afectar el rendimiento de un sistema. Linux, junto con la familia de sistemas operativos Windows, implementa el modelo uno a uno.
Figura 4.8 Modelo uno a uno.
4.3.3 Modelo de muchos a muchos
El modelo de muchos a muchos (Figura 4.9) multiplexa muchos hilos de nivel de usuario para un número menor o igual de hilos de kernel. El número de hilos del kernel puede ser específico para una aplicación particular o una máquina particular (una aplicación se puede asignar más hilos de kernel en un sistema con ocho núcleos de procesamiento de que un sistema con cuatro núcleos).
Figura 4.9 Modelo de muchos a muchos.
Consideremos el efecto de este diseño en la concurrencia. Mientras que el modelo muchos a uno le permite al desarrollador crear tantos hilos de usuario como desee, no resulta en paralelismo, porque el kernel solo puede planificar un hilo de kernel a la vez. El modelo uno a uno permite una mayor concurrencia, pero el desarrollador debe tener cuidado de no crear demasiados hilos dentro de una aplicación. (De hecho, en algunos sistemas, ella puede estar limitada en el número de hilos que puede crear.) El modelo de muchos a muchos no sufre ninguna de estas deficiencias: los desarrolladores pueden crear tantos hilos de usuario como sea necesario, y los hilos del kernel correspondientes pueden ejecutarse en paralelo en un multiprocesador. También, cuando un hilo realiza una llamada al sistema bloqueante, el kernel puede planificar otro hilo para ejecución.
Una variación en el modelo de muchos a muchos multiplexa muchos hilos niveles de usuario a un número menor o igual de hilos del kernel, pero también permite que un hilo de nivel de usuario que se vincule a un hilo del kernel. Esta variación es a veces denominado modelo de dos niveles (Figura 4.10).
Figura 4.10 Modelo de dos niveles.
Aunque el modelo de muchos a muchos parece ser el más flexible de los modelos discutidos, en la práctica es difícil de implementar. Además, con un creciente número de núcleos de procesamiento que aparecen en la mayoría de los sistemas, por lo que el límite del número de hilos del kernel se ha vuelto menos importante. Como resultado, la mayoría de los sistemas operativos ahora usan el modelo uno a uno. Sin embargo, como veremos en la Sección 4.5, algunas bibliotecas de concurrencia contemporáneas tienen identificadores de tareas que los desarrolladores luego tienen que asignar a hilos utilizando el modelo de varios a varios.
4.4 Bibliotecas de hilos
Diapositiva 16:
· La biblioteca de Hilos proporciona al programador una API para crear y administrar Hilos
· Dos formas principales de implementar
· Biblioteca completamente en espacio de usuario
· Biblioteca a nivel de kernel compatible con el sistema operativo
Una biblioteca de hilos proporciona al programador una API para crear y administrar hilos. Hay dos formas principales de implementar una biblioteca de hilos. 
· El primer enfoque es proporcionar una biblioteca completamente en el espacio del usuario sin apoyo del kernel. Todos los códigos y estructuras de datos para la biblioteca existen en el espacio del usuario. Esto significa que invocar una función en la biblioteca da como resultado una llamada de función local en espacio de usuario y no una llamada al sistema. 
· El segundo enfoque es implementar una biblioteca a nivel del kernel compatible directamente por el sistema operativo. En este caso, las estructuras de código y datos para la biblioteca ya existe en el espacio del kernel. Invocar una función en la API para la biblioteca típicamente resulta en una llamada del sistema al kernel.
Hoy se utilizan tres bibliotecas de hilos principales: hilos POSIX, Windows y Java. Pthreads, la extensión de hilos del estándar POSIX, se puede proporcionar como una biblioteca de nivel de usuario o de nivel de kernel. 
La biblioteca de hilos de Windows es una biblioteca disponible a nivel de kernel en sistemas Windows. 
La API de hilo Java permite que los hilos se creen y administren directamente en programas Java. Sin embargo, porque en la mayoría de los casos, la JVM se ejecuta sobre un sistema operativo host, la API de hilos de Java generalmente se implementa utilizando una biblioteca de hilos disponible en el sistema host Esto significa que en los sistemas Windows, los hilos de Java son típicamente implementados usando la API de Windows; luego en Sistemas UNIX, Linux y macOS normalmente usan Pthreads.
Para hilos POSIX y Windows, cualquier dato declarado globalmente, es decir, declarado fuera de cualquier función: se comparten entre todos los hilos que pertenecen al mismo proceso. Debido a que Java no tiene una noción equivalente de datos globales, acceder a los datos compartidos se debe organizar explícitamente entre hilos.
En el resto de esta sección, describimos la creación básica de hilos utilizando estas tres bibliotecas de hilos. Como ejemplo, diseñamos un programa multihilo que realiza la suma de un número entero no negativo en un hilo separado usando la conocida función de sumatoria: 
Por ejemplo, si N fuera 5, esta función representaría la suma de enteros del 1 al 5, que es 15. Cada uno de los tres programas se ejecutará con los límites superiores de la suma ingresada en la línea de comando. Por lo tanto, si el
el usuario ingresa 8, se generará la suma de los valores enteros de 1 a 8.
Antes de continuar con nuestros ejemplos de creación de hilos, presentamos dos estrategias generales para crear múltiples hilos: 
hilos asincrónicos e 
hilos sincrónicos. 
Con hilos asincrónicos, una vez que el padre crea un hilo hijo, el padre reanuda su ejecución, de modo que el padre y el hijo se ejecutan concurrente e independientemente el uno del otro. Porque los hilos son independientes, por lo general, hay poco intercambio de datos entre ellos. El hilado asincrónico es la estrategia utilizada en el servidor multihilo ilustrado en la Figura 4.2 y también se usa comúnmente para diseñar interfaces interactivas de usuario.
El hilo sincrónico se produce cuando el hilo principal crea uno o más hilos hijos y luego debe esperar a quetodos sus hijos terminen antes de que se reanude. Aquí, los hilos creados por el padre realizan el trabajo al mismo tiempo, pero los padres no pueden continuar hasta que este trabajo de los hijos se haya completado. Una vez que cada hilo
Finaliza su trabajo, termina y se une con su padre. Solo después de todo los hilos hijos se han unido puede el padre reanudar la ejecución. Típicamente el hilado síncrono implica un intercambio de datos significativo entre los hilos. Por ejemplo, el hilo principal puede combinar los resultados calculados por sus diversos hilos hijos. Todos los siguientes ejemplos usan hilos sincrónicos.
4.4.1 Pthreads
Pthreads se refiere al estándar POSIX (IEEE 1003.1c) que define una API para la Creación y sincronización de hilos. Esta es una especificación para el comportamiento del hilo, no una implementación. Los diseñadores de sistemas operativos pueden implementar la especificación, de cualquier manera que deseen.
Numerosos sistemas implementan la especificación Pthreads; la mayoría son sistemas de tipo UNIX, incluidos Linux y macOS. Aunque Windows no admite Pthreads de forma nativa, algunas implementaciones de terceros para Windows están disponibles.
El programa C que se muestra en la Figura 4.11 muestra la API básica de Pthreads para construir un programa multihilo que calcule la suma de un entero no negativo en un hilo separado. En un programa Pthreads, hilos separados comienzan la ejecución en una función especificada. En la Figura 4.11, este es la función runner( )
Cuando comienza este programa, comienza un solo hilo de control en main().
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int sum; /* this data is shared by the thread(s) */
void *runner(void *param); /* threads call this function */
int main(int argc, char *argv[])
{
pthread t tid; /* the thread identifier */
pthread attr t attr; /* set of thread attributes */
/* set the default attributes of the thread */
pthread attr init(&attr);
/* create the thread */
pthread create(&tid, &attr, runner, argv[1]);
/* wait for the thread to exit */
pthread join(tid,NULL);
printf("sum = %d∖n",sum);
}
/* The thread will execute in this function */
void *runner(void *param)
{
int i, upper = atoi(param);
sum = 0;
for (i = 1; i <= upper; i++)
sum += i;
pthread exit(0);
}
Figura 4.11 Programa C multihilo que utiliza la API Pthreads.
Después de cierta inicialización, main () crea un segundo hilo que comienza en la función runner (). Ambos hilos comparten la suma de datos global.
Veamos más de cerca este programa. Todos los programas de Pthreads deben incluir el archivo de encabezado pthread.h. La declaración pthread_t tid declara el identificador para el hilo que crearemos. Cada hilo tiene un conjunto de atributos, incluyendo el tamaño de la pila y la información de planificación. El pthread_attr_t attr
La declaración representa los atributos para el hilo. Establecemos los atributos en la función que llama a pthread_attr_init (& attr). Porque no lo hicimos explícitamente, establece cualquier atributo, utilizamos los atributos predeterminados proporcionados. (En el Capítulo 5, nosotros discutiremos algunos de los atributos de planificación proporcionados por la API Pthreads.). Se crea un hilo separado con la llamada a la función pthread_create (). Adicionalmente para pasar el identificador de hilo y los atributos para el hilo, también pasamos el nombre de la función donde el nuevo hilo comenzará a ejecutarse, en este caso, la función runner (). Por último, pasamos el parámetro entero que era proporcionado en la línea de comando, argv [1].
En este punto, el programa tiene dos hilos: el hilo inicial (o padre) en main () y el hilo de suma (o hijo) que realiza la operación de suma en la función runner (). Este programa sigue con la estrategia del hilo de unir los hilos creados, según la cual después de crear el hilo de suma, el hilo padre espera a que termine llamando a la función pthread_join (). El hilo sumatoria terminará cuando llame a la función pthread_exit (). Una vez que el hilo de la suma regresa, el hilo padre mostrará el valor de suma de los datos compartidos
Este programa de ejemplo crea un solo hilo. Con el crecimiento dominante de los sistemas multinúcleo, escribir programas que contienen varios hilos se ha vuelto cada vez más común. Un método simple para esperar en varios
los hilos que usan la función pthread_join () es para encerrar la operación dentro de Un simple lazo for. Por ejemplo, puedes unir diez hilos usando el código de Pthread que se muestra en la Figura 4.12.
#define NUM THREADS 10
/* an array of threads to be joined upon */
pthread t workers[NUM THREADS];
for (int i = 0; i < NUM THREADS; i++)
pthread join(workers[i], NULL);
Figura 4.12 Código Pthread para unir diez hilos.
4.4.2 Hilos de Windows
La técnica para crear hilos usando la biblioteca de hilos de Windows es similar a la técnica Pthreads de varias maneras. Ilustramos el hilo de la API Windows en el programa C que se muestra en la Figura 4.13. Tenga en cuenta que debemos incluir el archivo de encabezado windows.h cuando se usa la API de Windows. 
Al igual que en la versión de Pthreads que se muestra en la Figura 4.11, los datos compartidos por hilos separados, en este caso, Sum, se declaran globalmente (los tipo de datos DWORD es un entero de 32 bits sin signo). También definimos la función Summation(), eso se realizará en un hilo separado. Esta función pasa un puntero a un void, que Windows define como LPVOID. El hilo que realiza esta función establece la suma global de datos en el valor de la suma desde 0 al parámetro pasado a Summation().
Los hilos se crean en la API de Windows utilizando la función CreateThread (), y, al igual que en Pthreads, se pasa un conjunto de atributos para el hilo a esta función. Estos atributos incluyen información de seguridad, el tamaño de pila, y un indicador que se puede configurar para indicar si el hilo debe comenzar en un estado de modo suspendido. En este programa, usamos los valores predeterminados para estos atributos. (los valores predeterminados no establecen inicialmente el hilo en un estado suspendido y en su lugar hacen que sea elegible para ser ejecutado por el planificador de la CPU.)
Una vez que el hilo de suma se crea, el padre debe esperar a que se complete antes de generar el valor de Suma, ya que el valor lo establece el hilo suma.
#include <windows.h>
#include <stdio.h>
DWORD Sum; /* data is shared by the thread(s) */
/* The thread will execute in this function */
DWORD WINAPI Summation(LPVOID Param)
{
DWORD Upper = *(DWORD*)Param;
for (DWORD i = 1; i <= Upper; i++)
Sum += i;
return 0;
}
int main(int argc, char *argv[])
{
DWORD ThreadId;
HANDLE ThreadHandle;
int Param;
Param = atoi(argv[1]);
/* create the thread */
ThreadHandle = CreateThread(
NULL, /* default security attributes */
0, /* default stack size */
Summation, /* thread function */
&Param, /* parameter to thread function */
0, /* default creation flags */
&ThreadId); /* returns the thread identifier */
/* now wait for the thread to finish */
WaitForSingleObject(ThreadHandle,INFINITE);
/* close the thread handle */
CloseHandle(ThreadHandle);
printf("sum = %d∖n",Sum);
}
Figura 4.13 Programa C multihilo que utiliza la API de Windows.
Recordemos que el programa Pthread (Figura 4.11) hizo que el hilo padre esperara el hilo de suma usando la declaración pthread_ join (). Realizamos el equivalente de esto en la API de Windows que utiliza la función WaitForSingleObject(), que provoca la creación del hilo para bloquearse hasta que el hilo de suma haya salido.
En situaciones que requieren esperar a que se completen varios hilos, se utiliza la función WaitForMultipleObjects(). Esta función se pasa cuatro parámetros:
1. El número de objetos a esperar
2. Un puntero al conjunto de objetos.
3. Una bandera que indica si todos los objetos ya han sido señalados
4. Una duración de tiempo de espera (o INFINITO)
Por ejemplo, si THandles es una matriz de objetos HANDLE de hilos de tamaño N, el
el hilo principal puede esperara que todos sus hilos secundarios se completen con esta declaración:
WaitForMultipleObjects(N, THandles, TRUE, INFINITE);
4.4.3 Hilos en Java
Los hilos son el modelo fundamental de ejecución del programa en un programa Java y el lenguaje Java y su API proporcionan un amplio conjunto de características para la creación y gestión de hilos. Todos los programas Java comprenden al menos un solo hilo de control, incluso un programa Java simple que consta de solo un método main() se ejecuta como un hilo único en la JVM. Los hilos de Java están disponibles en cualquier sistema que proporciona una JVM que incluye Windows, Linux y macOS. La API thread de java también está disponible para aplicaciones de Android.
Hay dos técnicas para crear hilos explícitamente en un programa Java. Un enfoque es crear una nueva clase que se deriva de la clase Thread y sustituir el método run (). Una alternativa, y más comúnmente utilizada —La técnica es definir una clase que implemente la interfaz Runnable y se debe definir un método run(). Esta interfaz define un único método abstracto con public void run(). El código en el método run () de una clase que implementa Runnable es lo que se ejecuta en un hilo separado. A continuación se muestra un ejemplo:
class Task implementa Runnable
{
public void run() {
System.out.println("I am a thread.");
}
}
La creación de hilos en Java implica crear un objeto hilo y pasarlo a una instancia de una clase que implementa Runnable, seguida de invocar el método start () en el objeto Thread. Esto aparece en el siguiente ejemplo:
Thread worker = new Thread(new Task());
worker.start();
Invocar el método start() para el nuevo objeto Thread hace dos cosas:
1. Asigna memoria e inicializa un nuevo hilo en la JVM.
2. Llama al método run(), haciendo que el hilo sea elegible para ser ejecutado por la JVM.
(Tenga en cuenta nuevamente que nunca llamamos al método run() directamente. Más bien, llamamos
el método start(), quien llama al método run() en nuestro nombre).
Recuerde que los hilos principales (padres) en las bibliotecas Pthreads y Windows usan pthread_join () y WaitForSingleObject() (respectivamente) para esperar a los hilos de suma que terminen antes de continuar. El método join() en Java proporciona una funcionalidad similar. (Tenga en cuenta que join() puede lanzar una InterruptedException, que elegimos ignorar)
try {
worker.join();
}
catch (InterruptedException ie) { }
Si el padre debe esperar a que terminen varios hilos, el método join() puede ser encerrado en un bucle for similar al que se muestra para Pthreads en la Figura 4.12.
EXPRESIONES LAMBDA EN JAVA
A partir de la versión 1.8 del lenguaje, Java introdujo expresiones Lambda, que permiten una sintaxis mucho más limpia para crear hilos. Más bien que definiendo una clase separada que implementa Runnable, una expresión Lambda se puede usar en su lugar:
Runnable task = () -> {
System.out.println("I am a thread.");
};
Thread worker = new Thread(task);
worker.start();
Las expresiones lambda, así como funciones similares conocidas como clausuras, son una característica destacada de los lenguajes de programación funcionales y han estado disponible en varios lenguajes no funcionales, incluidos Python, C ++ y C#. Como veremos en ejemplos posteriores en este capítulo, expresiones de Lamdba a menudo proporcionan una sintaxis simple para desarrollar aplicaciones paralelas.
4.4.3.1 Marco de trabajo (Framework) Java 
Java ha soportado la creación de hilos utilizando el enfoque que hemos descrito desde sus orígenes. Sin embargo, comenzando con la Versión 1.5 y su API, Java introdujo varias características nuevas de concurrencia que brindan a los desarrolladores un control mucho mayor sobre la creación y comunicación de hilos. Estas herramientas están disponibles en el paquete java.util.concurrent. 
En lugar de crear explícitamente objetos Thread, la creación de hilos organizada alrededor de la interfaz del ejecutor:
public interface Executor
{
void execute(Runnable command);
}
Las clases que implementan esta interfaz deben definir el método execute (), que se pasa un objeto Runnable. Para los desarrolladores de Java, esto significa usar el Ejecutor en lugar de crear un objeto Thread separado e invocar su método inicio (). El ejecutor se usa de la siguiente manera:
Executor service = new Executor;
service.execute(new Task());
El marco (framework) Executor se basa en el modelo productor-consumidor; Las Tareas implementando la interfaz Runnable son las productoras, y los hilos que ejecutan estas tareas son los consumidores. La ventaja de este enfoque es que no sólo divide la ejecución creando hilos, sino también proporciona un mecanismo para la comunicación entre tareas concurrentes.
El intercambio de datos entre hilos que pertenecen al mismo proceso se da fácilmente en Windows y Pthreads, ya que los datos compartidos simplemente se declaran globalmente. Pero en Java, un lenguaje orientado a objetos puro, no tiene esa noción de datos globales. Nosotros podemos pasar parámetros a una clase que implementa Runnable, pero los hilos de Java No pueden devolver resultados. Para abordar esta necesidad, el paquete java.util.concurrent define adicionalmente la interfaz Callabe, que se comporta de manera similar a Runnable, excepto que se puede devolver un resultado. Resultados devueltos de las tareas Callable se conocen como objetos futuros. Se puede recuperar un resultado del método get() definido en la interfaz Future. El programa que se muestra en la Figura 4.14 ilustra el programa de suma utilizando estas características de Java.
La clase Summation implementa la interfaz Callabe, que especifica el método V call () - es el código en este método call () que se ejecuta en un hilo separado. Para ejecutar este código, creamos un objeto NewSingleThreadExecutor
(proporcionado como método estático en la clase Executors), que es de tipo ExecutorService, y pásele una tarea Callabe utilizando el método submit(). (La diferencia principal entre los métodos execute () y submit () es que el primero no devuelve ningún resultado, mientras que el segundo devuelve un resultado como un futuro.) Una vez que enviemos la tarea invocable al hilo, esperamos su resultado al llamar al método get() del objeto Future que devuelve.
Al principio es bastante fácil notar que aparece este modelo de creación de hilos más complicado que simplemente crear un hilo y unirse en su terminación. Sin embargo, incurrir en este modesto grado de complicación confiere beneficios. Como nosotros hemos visto, el uso de Callable y Future permite que los hilos devuelvan resultados.
import java.util.concurrent.*;
class Summation implements Callable<Integer>
{
private int upper;
public Summation(int upper) {
this.upper = upper;
}
/* The thread will execute in this method */
public Integer call() {
int sum = 0;
for (int i = 1; i <= upper; i++)
sum += i;
return new Integer(sum);
}
}
public class Driver
{
public static void main(String[] args) {
int upper = Integer.parseInt(args[0]);
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> result = pool.submit(new Summation(upper));
try {
System.out.println("sum = " + result.get());
} catch (InterruptedException | ExecutionException ie) { }
}
}
Figura 4.14 Ilustración de la API del framework Executor de Java
Además, este enfoque separa la creación de hilos de los resultados que produce: en lugar de esperar a que termine un hilo antes de recuperar los resultados, el padre sólo espera que los resultados estén disponibles. Finalmente,
Como veremos en la Sección 4.5.1, este marco puede combinarse con otras características para crear herramientas robustas para administrar una gran cantidad de hilos.
4.5 Hilado implícito
	
Con el continuo crecimiento del procesamiento multinúcleo, aplicaciones que contienen cientos, o incluso miles, de hilos se esperan en el futuro. Diseñar tales aplicaciones no es una tarea trivial: los programadores deben abordar no solo los desafíos descritos en la Sección 4.2 sinotambién dificultades adicionales. Estas dificultades, que se relacionan con la corrección del programa, están cubiertas en el Capítulo 6 y el Capítulo 8.
Una forma de abordar estas dificultades y apoyar mejor el diseño de aplicaciones concurrentes y paralelas es transferir la creación y gestión de hilos de desarrolladores de aplicaciones a compiladores y bibliotecas en tiempo de ejecución. Esta estrategia, denominada hilado implícito, es una tendencia cada vez más popular. En esta sección, exploramos cuatro enfoques alternativos para diseñar aplicaciones que pueden aprovechar los procesadores multinúcleo mediante hilos implícitos. Como veremos, estas estrategias generalmente requieren que los desarrolladores de aplicaciones identifiquen las tareas, no hilos, que pueden ejecutarse en paralelo. Una tarea generalmente se escribe como una función, que la biblioteca en tiempo de ejecución luego asigna a un hilo separado, típicamente usando el modelo de muchos a muchos (Sección 4.3.3). La ventaja de este enfoque es que los desarrolladores solo necesitan identificar las tareas paralelas, y las bibliotecas determinan los detalles específicos de la creación y gestión de hilos.
4.5.1 Grupos de hilos
En la Sección 4.1, describimos un servidor web multihilo. En esta situación, cada vez que el servidor recibe una solicitud, crea un hilo separado para atender para atender la solicitud. Mientras que crear un hilo separado es ciertamente mejor a crear un proceso separado, un servidor multihilo tiene algunos problemas potenciales.
El primer problema se refiere a la cantidad de tiempo requerida para crear el hilo, junto con el hecho de que el hilo se descartará una vez que se haya completado el trabajo. El segundo problema es más grave. Si permitimos que cada solicitud concurrente se le dé servicio en un nuevo hilo, y al no haber colocado un límite en el Número de hilos activos simultáneamente en el sistema: Hilos ilimitados podrían agotar los recursos del sistema, como el tiempo de CPU o la memoria. Una solución a este problema es utilizar un grupo de hilos.
LA JVM Y EL SISTEMA OPERATIVO
La JVM generalmente se implementa sobre un sistema operativo host (consulte la Figura 18.10). Esta configuración permite que la JVM oculte los detalles de implementación del sistema operativo subyacente y proporcione un entorno coherente que permita que los programas Java ejecuten en cualquier plataforma que admita una JVM. La especificación para la JVM no indica cómo los hilos de Java deben ser “mapeados” al sistema operativo subyacente, dejando en su lugar esa decisión a la implementación particular de la JVM. Por ejemplo, el sistema operativo Windows usa el modelo uno a uno; por lo tanto, cada hilo de Java para una JVM que se ejecuta “mapea” a un hilo del kernel de Windows. Adicionalmente, puede haber una relación entre la biblioteca de hilos de Java y la biblioteca de hilos en el sistema operativo host. Por ejemplo, implementaciones de una JVM para la familia de sistemas operativos Windows podría usar la API de Windows al crear hilos de Java; Los sistemas Linux y macOS pueden usar la API de hilos Pthreads 
La idea general detrás de un grupo de hilos es crear varios hilos, ponerlos en marcha y colocarlos en un grupo, donde se sientan y esperan el trabajo. Cuando el servidor recibe una solicitud, en lugar de crear un hilo, se envía la solicitud al grupo de hilos y reanuda la espera de solicitudes adicionales. Sí hay un hilo disponible en el grupo, se despierta y se atiende la solicitud inmediatamente. Si el grupo no contiene ningún hilo disponible, la tarea se pone en cola hasta que uno pasa a libre. Una vez que un hilo completa su servicio, vuelve al grupo y espera más trabajo. Los grupos de hilos funcionan bien cuando las tareas enviadas al grupo se pueden ejecutar de forma asíncrona.
Los grupos de hilos ofrecen estos beneficios:
1. Servir una solicitud con un hilo existente es a menudo más rápido que esperar la creación de un hilo
2. Un grupo de hilos limita el número de hilos que existen en cualquier momento. Esto es particularmente importante en sistemas que no pueden soportar una gran cantidad de hilos concurrentes.
3. Separar la tarea a realizar de la mecánica de creación de la tarea nos permite utilizar diferentes estrategias para ejecutar la tarea. Por ejemplo, la tarea podría planificarse para ejecutar después de un tiempo de retraso o para ejecutarse periódicamente
El número de hilos en el grupo se puede establecer en función de factores como la cantidad de CPUs en el sistema, la cantidad de memoria física y la cantidad esperada de solicitudes de clientes concurrentes. Las arquitecturas pueden ajustar dinámicamente el número de hilos en el grupo. De acuerdo con los patrones de uso. Dichas arquitecturas proporcionan el beneficio adicional de tener un grupo más pequeño, lo que consume menos memoria, cuando la carga en El sistema es bajo. Discutimos una de esas arquitecturas, la Gran Central de Despacho de Apple, más adelante en esta sección. La API de Windows proporciona varias funciones relacionadas con los grupos de hilos. Utilizar la API del grupo de hilos es similar a la creación de un hilo con la función Thread Create(), como se describe en la Sección 4.4.2. Aquí, una función que está por ejecutarse se define como un hilo separado. Tal función puede aparecer de la siguiente manera:
DWORD WINAPI PoolFunction(PVOID Param) {
/* this function runs as a separate thread. */
}
GRUPOS DE ANDROID
En la Sección 3.8.2.1, cubrimos las RPC en el sistema operativo Android. Usted puede recordar de esa sección que Android usa la definición de interfaz de Android Language (AIDL), una herramienta que especifica la interfaz remota que los clientes interactúan con el servidor. AIDL también proporciona un grupo de hilos. Un Servicio remoto usa el grupo de hilos que puede manejar múltiples solicitudes concurrentes, atendiendo a cada solicitud utilizando un hilo tomado del grupo.
Se pasa un puntero a PoolFunction() a una de las funciones en el grupo de hilos de API y un hilo del grupo ejecuta esta función. Uno de esos miembros en la API del grupo de hilos es la función QueueUserWorkItem(), que se pasa tres parámetros:
• Función LPTHREAD START ROUTINE: un puntero a la función que está por correr como un hilo separado
• Parámetro PVOID: el parámetro pasado a la función
• Banderas ULONG: banderas indicando cómo debe crear y administrar el grupo de hilos la ejecución del hilo
Un ejemplo de invocación de una función es el siguiente:
QueueUserWorkItem (& PoolFunction, NULL, 0);
Esto hace que un hilo del grupo de hilos invoque PoolFunction() en nombre del programador. En este caso, no pasamos parámetros a PoolFunction ().
Debido a que especificamos 0 como indicador, proporcionamos el grupo de hilos sin instrucciones especiales para la creación de hilos.
Otros miembros de la API del grupo de hilos de Windows incluyen utilidades que invocar funciones a intervalos periódicos o cuando una solicitud de E/S asíncrona sea completada.
4.5.1.1 Grupos de hilos de Java
El paquete java.util.concurrent incluye una API para unas variedades de arquitecturas de grupo de hilos. Aquí, nos centramos en los siguientes tres modelos:
1. Ejecutor de hilo único: newSingleThreadExecutor(): crea un grupo de tamaño 1.
2. Ejecutor de hilos fijo: newFixedThreadPool(int size): crea un grupo de hilos con un número especificado de hilos.
3. Ejecutor de hilos en caché — newCachedThreadPool() - crea un grupo de hilos como si fuera ilimitado, reutilizando hilos en muchos casos.
De hecho, ya hemos visto el uso de un grupo de hilos de Java en la Sección 4.4.3, donde creamos un NewSingleThreadExecutor en el ejemplo del programa se muestra en la figura 4.14. En esa sección, notamos que el marco de ejecución de Java puede usarse para construir herramientas de hilado más robustas. Ahora describimos cómo se puede usar para crear grupos de hilos.
Un grupo de hilos se crea utilizando uno de los métodos que construye en la clase Executors:
• Static ExecutorServicenewSingleThreadExecutor()
• Static ExecutorService newFixedThreadPool (int size)
• Static ExecutorService newCachedThreadPool ()
Cada uno de estos métodos de crea y devuelve una instancia de objeto que implementa la interfaz ExecutorService. La ExecutorService extiende interfaz Executor, lo que nos permite invocar el método execute() en este objeto. Además, ExecutorService proporciona métodos para gestionar la terminación del grupo de hilos.
El ejemplo que se muestra en la Figura 4.15 crea un grupo de hilos en caché y envía tareas que debe ejecutar un hilo en el grupo mediante el método execute (). Cuando se invoca el método shutdown(), el grupo de hilos rechaza tareas adicionales y se cierra una vez que todas las tareas existentes han completado la ejecución.
import java.util.concurrent.*;
public class ThreadPoolExample
{
public static void main(String[] args) {
int numTasks = Integer.parseInt(args[0].trim());
/* Create the thread pool */
ExecutorService pool = Executors.newCachedThreadPool();
/* Run each task using a thread in the pool */
for (int i = 0; i < numTasks; i++)
pool.execute(new Task());
/* Shut down the pool once all threads have completed */
pool.shutdown();
}
Figura 4.15 Creación de un grupo de hilos en Java.
4.5.2 Fork Join (unión de bifurcación)
La estrategia para la creación de hilos cubierta en la Sección 4.4 a menudo se conoce como el modelo de unión de fork. Recuerde que con este método, el hilo padre principal crea (se bifurca) uno o más hilos secundarios y luego espera a que los hijos terminen y se unan con él, en ese momento puede recuperar y combinar sus resultados. Este modelo sincrónico a menudo se caracteriza por la creación explícita de hilos, pero También es un excelente candidato para el hilado implícito. En la última situación, los hilos no se construyen directamente durante la etapa del fork; más bien, se eligen tareas paralelas. Este modelo se ilustra en la figura 4.16. Una biblioteca gestiona la cantidad de hilos que se crean y también es responsable de asignar tareas a hilos. De alguna manera, este modelo de unión con fork es una versión sincrónica de grupos de hilos, en los que una biblioteca determina el número real de hilos que se crearán, por ejemplo, utilizando las técnicas descritas en la Sección 4.5.1.
4.5.2.1 Fork Join en Java
Java introdujo una biblioteca fork-join en la Versión 1.7 de la API que está diseñada para ser utilizado con algoritmos recursivos de divide y vencerás como Quicksort y Mergesort. Al implementar algoritmos de divide y vencerás usando esta biblioteca, las tareas separadas se bifurcan durante el paso de división y se asignan subconjuntos más pequeños del problema original. Los algoritmos deben diseñarse de manera que estas tareas separadas pueden ejecutarse simultáneamente. En algún momento, el tamaño del problema asignado a una tarea es lo suficientemente pequeño como para poder resolverla directamente y no requiere crear tareas adicionales.
Figura 4.16 Paralelismo de fork join.
El algoritmo recursivo general detrás del modelo de fork de Java se muestra a continuación:
Task(problem)
if problem is small enough
solve the problem directly
else
subtask1 = fork(new Task(subset of problem)
subtask2 = fork(new Task(subset of problem)
result1 = join(subtask1)
result2 = join(subtask2)
return combined results
La figura 4.17 representa el modelo gráficamente.
Figure 4.17 Fork-join (Unión de bifurcación) en Java.
Ahora ilustramos la estrategia de unión de bifurcación de Java mediante el diseño de un algoritmo divide y vencerás que suma todos los elementos en una matriz de enteros. En la versión 1.7 de la API Java introdujo un nuevo grupo de hilos, el ForkJoinPool, que le pueden asignar tareas que heredan de la clase base abstracta ForkJoinTask (que ahora asumiremos que es la clase SumTask). Lo siguiente crea un ForkJoin- Agrupa el objeto y envía la tarea inicial a través de su método invoke():
ForkJoinPool pool = new ForkJoinPool();
// array contains the integers to be summed
int[] array = new int[SIZE];
SumTask task = new SumTask(0, SIZE - 1, array);
int sum = pool.invoke(task);
Una vez finalizada, la llamada inicial a invoke() devuelve la suma de la matriz. 
La clase SumTask, que se muestra en la Figura 4.18, implementa un algoritmo divide y vencerás que suma el contenido de la matriz utilizando fork-join. Nuevas tareas se crean utilizando el método fork(), y el método compute() especifica el cálculo que realiza cada tarea. El método compute ()se invoca hasta que puede calcular directamente la suma del subconjunto asignado. Las llamadas a join() bloquean hasta que se complete la tarea, sobre la cual join () devuelve los resultados calculados en compute ().
Observe que SumTask en la Figura 4.18 extiende RecursiveTask. La estrategia de unión de bifurcación de Java se organiza en torno a la clase base abstracta ForkJoinTask, y las clases RecursiveTask y RecursiveAction extienden esta clase. La diferencia fundamental entre estas dos clases es que RecursiveTask devuelve un resultado (a través del valor de retorno especificado en compute()) y RecursiveAction no devuelve un resultado Se ilustra la relación entre las tres clases en el diagrama de clase UML en la figura 4.19.
Figura 4.19 Diagrama de clases UML para Java fork-join.
Una cuestión importante a considerar es determinar cuándo el problema es " suficientemente pequeño" para ser resuelto directamente y ya no requerir de la creación de tareas adicionales.
En SumTask, esto ocurre cuando el número de elementos que se suman es menor que el valor UMBRAL (THRESHOLD), que en la Figura 4.18 hemos establecido arbitrariamente en 1,000. En la práctica, determinar cuándo se puede resolver un problema directamente requiere cuidado pruebas de tiempo, ya que el valor puede variar según la implementación.
Lo que es interesante en el modelo de unión de bifurcación de Java es la gestión de tareas en donde la biblioteca construye un grupo de hilos de trabajo y equilibra la carga de tareas entre los trabajadores disponibles. En algunas situaciones, hay miles de tareas, pero solo un puñado de hilos que realizan el trabajo (por ejemplo, un hilo separado para cada CPU). Además, cada hilo en un ForkJoinPool mantiene una cola de tareas que ha bifurcado, y si la cola de un hilo está vacía, puede robar una tarea de la cola de otro hilo usando un algoritmo de robo de trabajo, equilibrando así la carga de trabajo de las tareas entre todos los hilos.
import java.util.concurrent.*;
public class SumTask extends RecursiveTask<Integer>
{
static final int THRESHOLD = 1000;
private int begin;
private int end;
private int[] array;
public SumTask(int begin, int end, int[] array) {
this.begin = begin;
this.end = end;
this.array = array;
}
protected Integer compute() {
if (end - begin < THRESHOLD) {
int sum = 0;
for (int i = begin; i <= end; i++)
sum += array[i];
return sum;
}
else {
int mid = (begin + end) / 2;
SumTask leftTask = new SumTask(begin, mid, array);
SumTask rightTask = new SumTask(mid + 1, end, array);
leftTask.fork();
rightTask.fork();
return rightTask.join() + leftTask.join();
}
}
}
Figure 4.18 Fork-join calculation using the Java API.
4.5.3 OpenMP
OpenMP es un conjunto de directivas de compilación, así como una API para programas escritos en C, C ++ o FORTRAN que proporciona soporte para programación paralela en ambientes de memoria compartida. OpenMP identifica regiones paralelas como bloques de código eso puede correr en paralelo. Los desarrolladores de aplicaciones insertan directivas de compilación en su código en regiones paralelas, y estas directivas instruyen la ejecución de OpenMP biblioteca de tiempo de ejecución para ejecutar la región en paralelo. 
El siguiente programa C ilustra una directiva del compilador sobre la región paralela que contiene la declaración printf ():
#include <omp.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
/* sequential code */
#pragma omp parallel
{
printf("I am a parallel region.");
}
/* sequential code */
return 0;
}
Cuando OpenMPencuentra la directiva
#pragma omp parallel
crea tantos hilos como núcleos de procesamiento haya en el sistema. Por lo tanto, para un sistema de doble núcleo, se crean dos hilos; para un sistema de cuatro núcleos, cuatro son creados; Etcétera. Todos los hilos ejecutan simultáneamente en la región paralela. A medida que cada hilo sale de la región paralela, se termina.
OpenMP proporciona varias directivas adicionales para ejecutar regiones de código en paralelo, incluyendo bucles de paralelización. Por ejemplo, supongamos que tenemos dos matrices, ayb, de tamaño N. Deseamos sumar sus contenidos y colocar los resultados en el arreglo c. Podemos ejecutar esta tarea en paralelo usando el siguiente código segmento, que contiene la directiva del compilador para paralelizar bucles:
#pragma omp parallel for
for (i = 0; i < N; i++) {
c[i] = a[i] + b[i];
}
OpenMP divide el trabajo contenido en el bucle for entre los hilos que tiene creados en respuesta a la directiva
#pragma omp parallel for
Además de proporcionar directivas para la paralelización, OpenMP permite a desarrolladores elegir entre varios niveles de paralelismo. Por ejemplo, ellos puede establecer el número de hilos manualmente. También permite a los desarrolladores identificar si los datos se comparten entre hilos o son propios de un hilo. OpenMP está disponible en varios compiladores de código abierto y comerciales para Linux, Windows, y sistemas macOS. Alentamos a los lectores interesados en aprender más sobre OpenMP y consultar la bibliografía al final del capítulo.
4.5.4 Grand Central Dispatch
Grand Central Dispatch (GCD) es una tecnología desarrollada por Apple para sus Sistemas operativos macOS e iOS. Es una combinación de una biblioteca en tiempo de ejecución, una API y extensiones de lenguaje que permiten a los desarrolladores identificar secciones de código (tareas) para ejecutar en paralelo. Al igual que OpenMP, GCD gestiona la mayoría de los detalles de hilado.
GCD planifica las tareas para que la ejecución (en el run-time) (en tiempo de ejecución) colocándolas en una cola de despacho. Cuando elimina una tarea de una cola, asigna la tarea a un hilo de un grupo de hilos que gestiona. GCD identifica dos tipos de Colas de despacho: seriales y concurrentes.
Las tareas colocadas en una cola en serie se eliminan en orden FIFO. Una vez que una tarea ha sido eliminada de la cola, debe completar la ejecución antes de que otra tarea sea removida. Cada proceso tiene su propia cola en serie (conocida como su cola principal), y los desarrolladores pueden crear colas en serie adicionales que son locales para un determinado proceso. (Esto es por qué las colas en serie también se conocen como colas de despacho privadas).
Las colas en serie son útiles para garantizar la ejecución secuencial de varias tareas.
Las tareas colocadas en una cola concurrente también se eliminan en orden FIFO, pero se pueden eliminar varias tareas a la vez, lo que permite ejecutar múltiples tareas en paralelo. Hay varias colas concurrentes en todo el sistema (también conocidas como colas de despacho global), que se dividen en cuatro calidades primarias de clases de servicio:
• QOS_CLASS_USER_ INTERACTIVE: la clase usuario interactivO del representa tareas que interactúan con el usuario, como la interfaz de usuario y el manejo de eventos, para garantizar una interfaz de usuario receptiva. Completar una tarea que pertenece a esta clase debería requerir solo una pequeña cantidad de trabajo.
• QOS_CLASS_USER_ INITIATED: la clase iniciada por el usuario es similar a la clase interactiva con el usuario en donde las tareas están asociadas con la interfaz de respuesta de usuario; sin embargo, las tareas iniciadas por el usuario pueden requerir a veces un procesamiento más largo. Abrir un archivo o una URL es una tarea iniciada por el usuario, por ejemplo. Tareas pertenecientes a esta clase deben completarse para que el usuario continúe interactuando con el sistema, pero no necesitan ser atendidas tan rápido como las tareas en la cola interactiva del usuario.
• QOS_CLASS_UTILITY: la clase de utilidad representa tareas que requieren más tiempo para completar, pero no exigen resultados inmediatos. Esta clase incluye trabajos como la importación de datos.
• QOS_CLASS_BACKGROUND: las tareas que pertenecen a la clase de “fondo” no son visibles para el usuario y no es sensible al tiempo. Los ejemplos incluyen la clasificación de un sistema de buzón y realización de copias de seguridad.
Las tareas enviadas a las colas de despacho se pueden expresar en una de dos caminos diferentes:
1. Para los lenguajes C, C ++ y Objective-C, GCD se identifica un lenguaje extensión conocido como bloque, que es simplemente una unidad autónoma de trabajo. Un bloque se especifica mediante un símbolo de intercalación ˆ insertado delante de un par de llaves {}. El código dentro de las llaves identifica la unidad de trabajo a realizar. A continuación se muestra un ejemplo simple de un bloque:
^{ printf("I am a block"); }
2. Para el lenguaje de programación Swift, una tarea se define usando una clausura, que es similar a un bloque en el sentido de que expresa una unidad autónoma de funcionalidad. Sintácticamente, una clausura Swift se escribe de la misma manera que un bloque, menos el cursor principal.
El siguiente segmento de código Swift ilustra la obtención de una cola concurrente para la clase iniciada por el usuario y el envío de una tarea a la cola usando la función de dispatch_async():
let queue = dispatch get global queue
(QOS CLASS USER INITIATED, 0)
dispatch async(queue,{ print("I am a closure.") })
Internamente, el grupo de hilos de GCD está compuesto por hilos POSIX. GCD activamente gestiona el grupo, permitiendo que la cantidad de hilos crezca y se reduzca de acuerdo con la demanda de la aplicación y la capacidad del sistema. GCD es implementado por la biblioteca libdispatch, que Apple ha lanzado bajo la licencia Apache Commons. Desde entonces ha sido ubicado al sistema operativo FreeBSD.
4.5.5 Bloques de creación de Intel Thread
Intel threading building blocks (TBB) es una biblioteca de plantillas que admite el diseño de aplicaciones paralelas en C ++. Como se trata de una biblioteca, que no requiere ningún compilador especial o lenguaje de soporte. Los desarrolladores especifican tareas que pueden ejecutarse en paralelo, y el planificador de tareas TBB asigna estas tareas en hilos subyacentes. Además, el planificador de tareas proporciona equilibrio de carga y es consciente de la memoria caché, lo que significa que dará prioridad a las tareas que probablemente tengan sus datos almacenados
en la memoria caché y, por lo tanto, se ejecutarán más rápidamente. TBB proporciona un conjunto rico
de características, incluidas plantillas para estructuras de bucles paralelos, operaciones atómicas, y bloqueo de exclusión mutua. Además, proporciona estructuras de datos concurrentes, incluyendo un mapa hash, cola y vector, que puede servir como versiones equivalentes seguras para hilos de las estructuras de datos de la biblioteca de plantillas estándar de C ++.
Usemos paralelismo para bucles como ejemplo. Inicialmente, suponga que hay un nombre aplicado a una función (valor flotante) que realiza una operación en el parámetro valor. Si tuviéramos una matriz v de tamaño n que contiene valores flotantes, podríamos usar el siguiente for loop para pasar cada valor en v a la función apply ():
for (int i = 0; i < n; i++) {
apply(v[i]);
}
Un desarrollador podría aplicar manualmente el paralelismo de datos (Sección 4.2.2) en un sistema multinúcleo asignando diferentes regiones de la matriz v a cada procesamiento núcleo; sin embargo, esto vincula estrechamente la técnica para lograr paralelismo al hardware físico, y el algoritmo tendría que ser modificado y recompilado por el número de núcleos de procesamiento en cada arquitectura específica.
Alternativamente, un desarrollador podría usar TBB, que proporciona una plantilla paralela que espera dos valores:
parallel for (range body )
donde rangose refiere al rango de elementos que serán iterados (conocido como espacio de iteración) y cuerpo especifica una operación que se realizará en un subrango de elementos.
Ahora podemos reescribir el bucle en serie anterior usando el paralelo TBB para plantilla de la siguiente manera:
parallel for (size t(0), n, [=](size t i) {apply(v[i]);});
Los dos primeros parámetros especifican que el espacio de iteración es de 0 a n − 1 (que corresponde al número de elementos en la matriz v). El segundo parámetro es una función lambda de C ++ que requiere un poco de explicación. La expresión [=] (size_t i) tiene el parámetro i, que asume cada uno de los valores sobre el espacio de iteración (en este caso de 0 a 𝚗 - 1). Cada valor de i se usa para identificar qué elemento de matriz en v se pasará como parámetro a la aplicación función (v [i]).
La biblioteca TBB dividirá las iteraciones del bucle en "fragmentos" separados y crear una serie de tareas que operan en esos fragmentos. (La función parallel_for permite a los desarrolladores especificar manualmente el tamaño de los fragmentos si lo desea) TBB también creará una serie de hilos y asignará tareas a los hilos disponibles. Esto es bastante similar a la biblioteca fork-join en Java. La ventaja de este enfoque es que solo requiere que los desarrolladores identifiquen qué operaciones puede ejecutarse en paralelo (especificando un paralelo para el bucle), y administra los detalles de la biblioteca que involucra al dividir el trabajo en tareas separadas que se ejecutan en paralelo. Intel TBB tiene versiones comerciales y de código abierto que se ejecutan en Windows, Linux y macOS. Consulte la bibliografía para obtener más detalles sobre Cómo desarrollar aplicaciones paralelas usando TBB.
4.6 Problemas de hilos
En esta sección, discutimos algunos de los temas a considerar en el diseño de programas con hilos múltiples
4.6.1 Las llamadas al sistema fork () y exec ()
En el Capítulo 3, describimos cómo se usa la llamada al sistema fork () para crear un proceso separado y duplicado. La semántica de las llamadas al sistema fork () y exec () cambian en un programa multihilo.
Si un hilo en un programa llama a fork (), ¿ hace que todos los hilos se dupliquen en el nuevo proceso?, o el nuevo proceso es de un solo hilo? Algunos sistemas UNIX han elegido tener dos versiones de fork (), una que duplica todos los hilos y otro que duplica solo el hilo que invoca a la llamada al sistema fork ().
La llamada al sistema exec () generalmente funciona de la misma manera que se describe en el Capítulo 3. Es decir, si un hilo invoca la llamada al sistema exec (), el programa especificado en el parámetro exec () reemplazará todo el proceso, incluido todos los temas.
Cuál de las dos versiones de fork () usar? depende de la aplicación..
Si se llama a exec () inmediatamente después de bifurcar, entonces duplicar todos los hilos es innecesario, ya que el programa especificado en los parámetros de exec () reemplazará el proceso. En este caso, duplicar solo el hilo de llamada es apropiado. Sin embargo, si el proceso separado no llama a exec () después de bifurcar, el proceso separado debe duplicar todos los hilos.
4.6.2 Manejo de señales
Una señal (signal) se utiliza en sistemas UNIX para notificar a un proceso que un evento particular ha ocurrido. Se puede recibir una señal sincrónica o asincrónicamente, dependiendo de la fuente y el motivo del evento que se señala. Todas las señales, sincrónicas o asincrónicas, siguen el mismo patrón:
1. Una señal es generada por la ocurrencia de un evento particular.
2. La señal se entrega a un proceso.
3. Una vez entregada, la señal debe ser manejada.
Ejemplos de señales sincrónicas incluyen acceso ilegal de memoria y división en 0. Si un programa en ejecución realiza cualquiera de estas acciones, se genera una señal. Las señales síncronas se entregan al mismo proceso que realizó la operación que causó la señal (esa es la razón por la que se consideran sincrónico).
Cuando una señal es generada por un evento externo a un proceso en ejecución, El proceso recibe la señal de forma asíncrona. Los ejemplos de tales señales incluyen finalizar un proceso con pulsaciones de teclas específicas (como <control> <C>) y Tener un temporizador para terminar. Por lo general, se envía una señal asincrónica a otro proceso.
Una signal puede ser manejada por uno de los dos posibles manejadores:
1. Manejador de señal predeterminado
2. Manejador de señal definido por el usuario
Cada señal tiene un controlador de señal predeterminado que el kernel ejecuta al manejar esa señal Esta acción predeterminada puede ser anulada por un controlador de señal definido por el usuario que se llama para manejar la señal. Las señales se manejan en diferentes formas. Algunas señales pueden ser ignoradas, mientras que otras (por ejemplo, un acceso ilegal a la memoria) se manejan terminando el programa.
El manejo de señales en programas de un solo hilo es sencillo: señales siempre se entregan a un proceso. Sin embargo, entregar señales en programas multihilos es más complicado, donde un proceso puede tener varios hilos.
¿Dónde, entonces, se debe entregar una señal?
En general, existen las siguientes opciones:
1. Entregue la señal al hilo al que se aplica la señal.
2. Entregue la señal a cada hilo en el proceso.
3. Entregue la señal a ciertos hilos en el proceso.
4. Asigne un hilo específico para recibir todas las señales para el proceso.
El método de cómo entregar una señal depende del tipo de señal generada. Por ejemplo, las señales síncronas deben ser entregadas al hilo que causa la señal y no a otros hilos en el proceso. Sin embargo, la situación con
Las señales asincrónicas no son tan claras. Algunas señales asincrónicas, como un señal que finaliza un proceso (<control> <C>, por ejemplo) —debe ser enviada a todos los hilos.
La función estándar de UNIX para entregar una señal es
kill (pid_t pid, int signal)
Esta función especifica el proceso (pid) hacia el cual se encuentra una señal particular (signal). La mayoría de las versiones multihilo de UNIX permiten que un hilo especifique qué señales aceptará y cuáles bloqueará. Por lo tanto, en algunos casos, una señal asincrónica puede ser entregada sólo a aquellos hilos que no son bloqueantes. Sin embargo, debido a que las señales deben manejarse solo una vez, una señal es típicamente entregada solo al primer hilo encontrado que no lo está bloqueando. POSIX Pthreads proporciona la siguiente función, que permite que se entregue una señal a un hilo específico (tid):
pthread kill (pthread t tid, int signal)
Aunque Windows no proporciona explícitamente soporte para señales, sí nos permite emularlos usando llamadas a procedimientos asíncronos (APC). El recurso APC permite que un hilo de usuario especifique una función que se llamará cuando el hilo del usuario recibe una notificación de un evento en particular. Como se indica por su nombre, un APC es más o menos equivalente a una señal asincrónica en UNIX. Sin embargo, mientras que UNIX debe lidiar con cómo manejar las señales en un entorno multihilo, el servicio de APC es más sencilla, ya que un APC se entrega a un hilo particular en lugar de a un proceso.
4.6.3 Cancelación de hilos
La cancelación de hilos implica terminar un hilo antes de que se haya completado. Por ejemplo, si varios hilos buscan simultáneamente en una base de datos y un hilo devuelve el resultado, los hilos restantes podrían cancelarse.
Otra situación podría ocurrir cuando un usuario presiona un botón en un navegador web que impide que una página web se cargue más. A menudo, se carga una página web usando varios hilos: cada imagen se carga en un hilo separado. Cuando el usuario presiona el botón de detener en el navegador, todos los hilos que cargan la página son cancelados
Un hilo que se cancelará se denomina hilo objetivo.
La cancelación de un hilo objetivo puede ocurrir en dos escenarios diferentes:
1. Cancelación asincrónica. Un hilo termina inmediatamente el objetivo hilo.
2. Cancelación diferida.El hilo objetivo verifica periódicamente si debería terminar, permitiéndole la oportunidad de terminar de una manera ordenada.
La dificultad con la cancelación ocurre en situaciones donde los recursos han sido asignados a un hilo cancelado o donde se cancela un hilo mientras está en medio de la actualización de datos que comparte con otros hilos. Esto se convierte especialmente en un problema con la cancelación asincrónica. A menudo, el sistema operativo reclamará los recursos del sistema de un hilo cancelado pero no reclamará todos los recursos. Por lo tanto, cancelar un hilo asincrónicamente puede no liberar los recursos necesarios para todo el sistema.
Con la cancelación diferida, en contraste, un hilo objetivo indica que se debe cancelar el hilo, pero la cancelación se produce solo después de que el hilo objetivo marcó una bandera para determinar si se debe cancelar o no. La amenaza puede realizar esta verificación en un punto en el que se puede cancelar de forma segura.
En Pthreads, la cancelación de hilos se inicia usando la función pthread cancel (). El identificador del hilo objetivo se pasa como un parámetro a la función. El siguiente código ilustra la creación, y luego la cancelación, de un hilo:
pthread t tid;
/* create the thread */
pthread create(&tid, 0, worker, NULL);
. . .
/* cancel the thread */
pthread cancel(tid);
/* wait for the thread to terminate */
pthread join(tid,NULL);
Invocar pthread cancel() indica sólo una solicitud para cancelar el hilo objetivo, sin embargo; la cancelación real depende de cómo se establece el hilo objetivo para manejar la solicitud. Cuando el hilo objetivo finalmente se cancela, la llamada pthread join () en el hilo de cancelación se devuelve. Pthreads admite tres modos de cancelación Cada modo se define como un estado y un tipo, como se ilustra en la tabla de abajo. Un hilo puede establecer su estado y tipo de cancelación utilizando una API.
Como ilustra la tabla, Pthreads permite que los hilos deshabiliten o habiliten la cancelación. Obviamente, un hilo no se puede cancelar si la cancelación está desactivada. Sin embargo, las solicitudes de cancelación siguen pendientes, por lo que el hilo puede habilitar la cancelación y responder a la solicitud.
El tipo de cancelación predeterminado es la cancelación diferida. Sin embargo, la cancelación ocurre solo cuando un hilo llega a un punto de cancelación. Muchas de las llamadas al sistema en POSIX y la biblioteca estándar de C se definen como puntos de cancelación, y estos se enumeran al invocar el comando man pthreads en un Sistema Linux. Por ejemplo, la llamada al sistema read () es un punto de cancelación que permite cancelar un hilo que está bloqueado mientras espera la entrada de read ().
Una técnica para establecer un punto de cancelación es invocar la Función pthread testcancel (). Si se determina que una solicitud de cancelación está pendiente, la llamada a pthread testcancel () no regresará, y el hilo terminará; de lo contrario, la llamada a la función volverá y el hilo continuará ejecutando. Además, Pthreads permite una función llamada controlador de limpieza si se cancela un hilo. Esta función permite que cualquier recurso que un hilo haya adquirido sea liberado antes de que el hilo sea terminado.
El siguiente código ilustra cómo un hilo puede responder a una Solicitud de cancelación de cancelación diferida:
while (1) {
/* do some work for awhile */
. . .
/* check if there is a cancellation request */
pthread testcancel();
}
Debido a los problemas descritos anteriormente, la cancelación asincrónica no es recomendado en la documentación de Pthreads. Por lo tanto, no lo cubrimos aquí. Una nota interesante es que en los sistemas Linux, la cancelación de hilos utilizando La API de Pthreads se maneja a través de señales (Sección 4.6.2).
La cancelación de hilos en Java utiliza una política similar a la cancelación diferida en Pthreads. Para cancelar un hilo de Java, invoque el método interrupt (), que establece el estado de interrupción del hilo objetivo en verdadero:
Thread worker;
. . .
/* set the interruption status of the thread */
worker.interrupt()
Un hilo puede verificar su estado de interrupción invocando el método isInterrupted (), que devuelve un valor booleano del estado de la interrupción de un hilo:
while (!Thread.currentThread().isInterrupted()) {
. . .
}
4.6.4 Almacenamiento local de hilos
Los hilos que pertenecen a un proceso comparten los datos del proceso. De hecho, esto es uno de los beneficios de la programación multihilo: proporciona la compartición de datos.
Sin embargo, en algunas circunstancias, cada hilo puede necesitar su propia copia de ciertos datos. Llamaremos a dicho almacenamiento local de hilos de datos (o TLS). Por ejemplo, en un sistema de procesamiento de transacciones, podríamos atender cada transacción en un hilo separado Además, a cada transacción se le puede asignar un identificador único Para asociar cada transacción de hilo con su identificador único, podríamos usar almacenamiento local de hilos.
Es fácil confundir TLS con variables locales. Sin embargo, las variables locales son visibles solo durante una invocación de una sola función, mientras que los datos TLS son visible a través de invocaciones de funciones. Además, cuando el desarrollador no tiene control sobre la creación de hilos, por ejemplo, cuando se utiliza una técnica implícita como un grupo de hilos, entonces se necesita un enfoque alternativo.
De alguna manera, TLS es similar a los datos estáticos; la diferencia es que en TLS Los datos son únicos para cada hilo. (De hecho, TLS generalmente se declara como estático).
La mayoría de las bibliotecas y compiladores de hilos brindan soporte para TLS. Por ejemplo, Java proporciona una clase ThreadLocal <T> con los métodos set () y get () para objetos ThreadLocal <T>. Pthreads incluye el tipo pthread_key_t, que proporciona una clave que es específica para cada hilo. Esta clave se puede usar para acceder a Datos TLS. El lenguaje C # de Microsoft simplemente requiere agregar el atributo de almacenamiento [ThreadStatic] para declarar datos locales de hilos. El compilador gcc proporciona al hilo la palabra clave de clase de almacenamiento para declarar datos TLS. Por ejemplo, si nosotros quisiéramos asignar un identificador único para cada hilo, lo declararíamos como sigue:
static thread int threadID;
4.6.5 Activaciones del planificador
Una cuestión final a considerar con los programas multihilo se refiere a la comunicación entre el kernel y la biblioteca de hilos, que puede ser necesaria por los modelos de muchos a muchos y de dos niveles discutidos en la Sección 4.3.3. Tal coordinación permite que el número de hilos del kernel se ajuste dinámicamente para ayudar a garantizar el mejor rendimiento.
Muchos sistemas que implementan el modelo muchos a muchos o el de dos niveles, colocan una estructura de datos intermedia entre los hilos de usuario y de kernel. Esta estructura de datos, típicamente conocida como un proceso liviano, o LWP: se muestra en la Figura 4.20. Para la biblioteca de hilos de usuario, el LWP parece ser un procesador virtual en el que la aplicación puede planificar un hilo de usuario para ejecutar. Cada LWP está conectado a un hilo del kernel, y son los hilos del kernel los que son planificados por el sistema operativo para ejecutarse en procesadores físicos. Si un hilo del kernel se bloquea (como cuando espera que se complete una operación de E/S), los bloques LWP también se bloquean. Arriba de la cadena, el hilo de nivel de usuario conectado al LWP también se bloquea.
Una aplicación puede requerir cualquier número de LWP para ejecutarse de manera eficiente. Una aplicación limitada por CPU que se ejecuta en un único procesador. En este escenario, sólo un hilo puede ejecutarse a la vez, por lo que un LWP es suficiente. Una aplicación que es limitada por I/O sin embargo, puede requerir múltiples LWP para ejecutarse. Típicamente, un LWP es requerido para cada llamada de sistema de bloqueo concurrente. Supongamos, por ejemplo,

Continuar navegando