Lenguajes de Programación I Año 2006 UNIVERSIDAD NACIONAL DEL CENTRO DE LA PROVINCIA DE BUENOS AIRES FACULTAD DE CIENCIAS EXACTAS FUNDAMENTOS BÁSICOS DE THREADS por José Macchi & Martín Mezzanotte ANTES DE COMENZAR… En este tutorial nos proponemos brindar una ayuda al alumno para familiarizarse con el concepto de thread, su significado, aspectos teóricos y características. Además examinaremos aspectos de multiprocesamiento (y multithreading), estudiaremos someramente algunos conceptos básicos de teoría de threads y observaremos cómo los threads pueden ser efectivamente usados para crear programas más eficientes. A tal efecto, nos centraremos en los aspectos de funcionamiento de los programas de múltiples threads, así como en las distintas instancias de procesamiento y administración de los mismos. ¿QUÉ ES UN THREAD? Un thread o hilo es un “semi-proceso”, que tiene su propia pila, y que ejecuta una porción de código dada. A diferencia de un proceso real, un thread normalmente comparte su memoria con otros threads (en la cual, tal como sucede con los procesos, cada thread tendrá asignado su espacio de memoria). Un grupo de threads es un conjunto de “hilos de ejecución” que están corriendo todos dentro del mismo proceso. Dado que comparten todos la misma porción de memoria, pueden acceder a las mismas variables globales, la misma memoria de heap, los mismos descriptores de archivos, etc. Todos corren en paralelo (por ejemplo: usando porciones del tiempo asignado al proceso en general o, si están dentro de un sistema con multiprocesadores, pueden eventualmente correr de forma paralela realmente). La ventaja de usar un grupo de threads en lugar de un programa normal en serie es que muchas operaciones pueden ser llevadas a cabo de forma paralela y, de esta forma, los eventos asociados a cada actividad pueden ser manejados inmediatamente tan pronto como llegan (por ejemplo: si tenemos un thread manejando la interface de usuario y otro manejando las consultas a una base de datos, podremos ejecutar consultas complejas realizadas por el usuario y aun así responder a la entrada del mismo mientras la consulta está siendo ejecutada). La ventaja de usar un grupo de threads en vez de un grupo de procesos es que el cambio de contexto entre threads es realizado mucho más rápidamente que el cambio de contexto entre procesos (un cambio de contexto significa que el sistema operativo Lenguajes de Programación I Año 2006 cambia la ejecución de un thread o proceso a la ejecución de otro). Por lo tanto, las comunicaciones entre dos threads son usualmente más rápidas y sencillas de implementar que las comunicaciones entre dos procesos. Por otro lado, debido a que los threads dentro de un grupo comparten el mismo espacio de memoria, si uno de ellos corrompe el espacio de su memoria, los otros threads también sufrirán las consecuencias. Con un proceso, el sistema operativo normalmente protege a un proceso de otros y si un proceso corrompe su espacio de memoria los demás no se verán afectados. BENEFICIOS DE THREADS VS PROCESOS Si se los implementa correctamente entonces los threads tienen algunas ventajas por sobre los (multi) procesos, es así que tendremos: Menos cantidad de tiempo para crear un nuevo thread respecto de un proceso, porque el nuevo thread usa la dirección de espacio del proceso actual. Menos tiempo en la terminación de un thread respecto de un proceso. Menos tiempo en el cambio de ejecución entre dos threads dentro del mismo proceso, debido a que ambos están usando el mismo espacio de direcciones del proceso. Menor cantidad de overhead en las comunicaciones; dado que la comunicación entre los threads de un proceso es simple y al compartir recursos comunes, particularmente el espacio de direcciones, la información creada por un thread estará disponible para todos los demás. MULTITHREADING VS. SINGLE THREADING Así como podemos tener múltiples procesos corriendo en nuestra PC, también podemos tener múltiples threads corriendo. De esta forma aparecen dos grandes grupos de sistemas operativos respecto de la forma de ejecución de threads. Single threading -- cuando el sistema operativo no reconoce al concepto de thread. Multithreading -- cuando el sistema operativo soporta múltiples threads de ejecución dentro de un proceso. La figura 1 se muestra la variedad de modelos de ejecución para threads y procesos. Lenguajes de Programación I Año 2006 Figura 1 – Modelos de Ejecución Algunos ejemplos de sistemas operativos populares para cada uno de los modelos presentados en la figura anterior son: MS-DOS -- soporta solo un único proceso con un único thread (cuadrante superior izquierdo de la figura). UNIX -- soporta múltiples procesos pero un único thread por proceso (cuadrante inferior izquierdo de la figura). Solaris -- soporta múltiples threads (en uno o más procesos). El Multithreading tiene ciertas ventajas respecto de Single Threading: Mejora la velocidad de respuesta de las aplicaciones. Utiliza los múltiples procesadores de una computadora (si los hubiere) más eficientemente. Mejora la estructura del programa; dado que muchos programas son más eficientemente estructurados si se los desarrolla como unidades de ejecución independientes o semi-independientes, en lugar de ser programas monolíticos. Usan menos recursos del sistema, fundamentalmente debido a que los espacios de direcciones se comparten y a que -por ello- la comunicación entre unidades de ejecución se simplifica. La Figura 2 ilustra los diferentes modelos de procesos y controles de threads en una aplicación single thread y multithreaded. Lenguajes de Programación I Año 2006 Figura 2 – Aplicaciones de Unico y Multiples Threads Como podemos observar en la Figura 2, los espacios de direcciones y el bloque de control del proceso son compartidos por los esquemas de múltiples threads; mientras que en el caso de los threads únicos esto no sucede, teniendo espacios y bloques de control únicos por cada proceso. NIVELES DE IMPLEMENTACION DE THREADS Desde el punto de vista de la teoría de Lenguajes de Programación, básicamente existen dos grandes categorías en lo que respecta a las implementaciones, manejos y administración de threads: Threads de Nivel del Lenguaje – Bibliotecas que administran Threads. Threads de Nivel del Sistema Operativo – Llamadas al sistema implementadas por los sistemas operativos. Ambos tipos de implementaciones tienen sus ventajas, y en realidad existen algunos sistemas operativos que permiten el acceso a ambos niveles (ejemplo: Solaris). Threads de Nivel del Lenguaje (TNL) En este nivel, el kernel del sistema operativo no se preocupa por la existencia de los threads. Toda la administración de los threads es realizada a nivel del lenguaje, es decir: por la aplicación, usando alguna biblioteca de threads. Los cambios entre Threads durantes la ejecución no requieren de la intervención del kernel, ni de sus privilegios y la planificación de los mismos es específica de la aplicación. Lenguajes de Programación I Año 2006 En algunos textos los TNL suelen denominarse también threads de nivel de usuario. Ventajas e inconvenientes de TNL Ventajas: El cambio entre threads no involucra al sistema operativo. La planificación puede ser especifica de la aplicación mejorando así la performance al seleccionar el algoritmo que más conveniente. TNLs pueden correr en cualquier sistema operativo, sólo necesitan de las bibliotecas de administración de threads. Desventajas: La mayoría de las llamadas al sistema son bloqueantes y como el kernel bloquea procesos (no conoce de threads) entonces terminará bloqueando a todos los threads dentro de un mismo proceso. El kernel puede sólo asignar procesadores a los procesos, por lo que dos threads dentro de un mismo proceso no podrán correr en forma simultánea, ya que disponen de un único procesador para correr. Threads de Nivel de Sistema Operativo (TNSO) A diferencia del nivel anterior, en este nivel la administración de los threads es realizada por el kernel del sistema operativo, por ende no existen bibliotecas de threads pero puede existir un API (de llamadas al sistema) que el kernel facilita para su utilización. El kernel mantiene la información del contexto de los procesos y los threads, es así que los cambios entre threads requieren la intervención del kernel en el proceso de planificación de la ejecución de los mismos. En algunos textos los TNSO suelen denominarse también threads de nivel de kernel. Ventajas e Inconvenientes de los TNSO Ventajas El kernel puede simultáneamente administrar varios threads de un mismo proceso en varios procesadores. Las rutinas del kernel pueden ser multithreaded. Desventajas: Los cambios entre threads dentro de un mismo proceso involucran la intervención del kernel, pudiendo generar demoras en el tiempo de ejecución. Aproximaciones combinadas de TNL/TNSO También existen modelos híbridos, donde la idea central es combinar lo mejor de cada una de las aproximaciones anteriores. Solaris es un ejemplo de un sistema operativo que combina TNL y TNSO. Lenguajes de Programación I Año 2006 ADMINISTRACIÓN DE LA MEMORIA CON THREADS Podemos decir que la planificación de la ejecución de los threads es una clara diferencia entre ambos modelos de implementación de threads: en el TNL la planificación es llevada a cabo a nivel del lenguaje, mientras que en el TNSO es llevada a cabo a nivel de sistema operativo. Sin embargo, aún no hemos estudiado a qué nivel se efectúa la administración de la memoria. Es decir: la manera en que cada thread accede a los datos compartidos por los restantes threads o a sus propios datos. Es importante señalar que en una aplicación de múltiples threads no todos los datos pueden ser compartidos entre los threads; sino que cada thread posee una pila propia de ejecución, donde cada uno de sus procedimientos o funciones alojan (en sus respectivos registros de activación) las variables semiestáticas, semidinámicas, descriptores, etc. Los datos almacenados en cada pila de ejecución sólo son accesibles por el thread propietario de dicha pila. En cambio, los datos almacenados en el heap y los datos almacenados en forma estática pueden ser compartidos por todos los threads del proceso; razón por la cual, puede ser necesario establecer mecanismos de sincronización que controlen el acceso a estos datos. En este esquema, la forma en que se administre el espacio de memoria del proceso es crucial para garantizar una ejecución correcta de la aplicación. Respecto de la administración de la memoria, puede decirse que el sistema operativo provee a la aplicación de una porción de memoria, a través de una cierta política que depende del sistema que utilicemos. Luego, esta porción de memoria debe se administrada (por el lenguaje o por el S.O.) en función de las características de la aplicación y del lenguaje de programación en cuestión. En este punto deben tomarse decisiones tales como ¿qué cantidad de memoria se asigna al proceso? ¿qué cantidad de memoria del proceso se asigna a cada thread? ¿cómo y dónde se ubican las pilas de ejecución de cada thread?; etc. Para contestar adecuadamente estos interrogantes es necesario conocer cierta información sobre la forma en que se ejecutarán los diferentes threads de la aplicación. Por ejemplo: si un thread necesita mucho más espacio de memoria que otros threads necesitaría ser alojado en un sector de memoria lo suficientemente amplio, como para atender sus demandas de memoria sin afectar a otras porciones de datos. Dado que no siempre es posible inferir este tipo de conocimiento, las estrategias de administración de memoria pueden diferir. Por ende, puede ocurrir que la administración de memoria sea llevada a cabo por el S.O. o por el Lenguaje. Frecuentemente, cuando no es posible conocer las características de ejecución de los threads, es el S.O. el que efectúa las tareas de administración de la memoria. Si, en cambio, el usuario del lenguaje (el programador) conoce esta información puede determinar la política de administración de memoria mediante instrucciones al compilador de la aplicación; o sea, establecer una administración a nivel del lenguaje. Lenguajes de Programación I Año 2006 IMPLEMENTACIONES Existen muchas y muy diferentes e incompatibles versiones de threading. Éstas incluyen implementaciones tanto a nivel de usuario como a nivel de kernel. Debemos notar que los trheads pueden ser implementados sin soporte del sistema operativo, aunque algunos sistemas operativos o bibliotecas proveen soporte explícito para ellos. Por ejemplo, las versiones recientes de Microsoft Windows (Windows NT 3.51 SP3 y posteriores) soportan el API de threads para aplicaciones que quieren mejorar su performance mediante el uso de sus propios esquemas de administración, en lugar de dejar dicha actividad al planificador del sistema operativo. Microsoft SQL Server 2000 en el modo de planificación de usuario es un ejemplo de ello. Ejemplos de implementaciones a nivel de Sistema Operativo Light Weight Kernel Threads en varios BSDs. M:N threading (en BSDs). Native POSIX Thread Library para Linux, una implementación de POSIX Threads (pthreads) standard. Apple Multiprocessing Services (en versiones 2.0 y posteriores). Ejemplos de Implementaciones de nivel de Lenguaje GNU Portable Threads [1] FSU Pthreads Apple Computer's Thread Manager Threads cooperativos de REALbasic Ejemplos de Implementaciones Híbridas Scheduler activations usadas por la biblioteca native POSIX de NetBSD BIBLIOGRAFÍA http://users.actcom.co.il/~choo/lupg/tutorials/debugging/debugging-with-gdb.html http://users.actcom.co.il/~choo/lupg/tutorials/multi-thread/multi-thread.html http://www.cs.cf.ac.uk/Dave/C/node29.html http://en.wikipedia.org/wiki/Thread_(computer_science)