lunes, 9 de abril de 2018

Hilos en C


1.     Introducción


En las primeras implementaciones de la computación, la programación manejaba procesos en un solo hilo de ejecución. Los programas se creaban en base a tarjetas perforadas o cintas, por lo cual su lectura era totalmente secuencial, en otras palabras, también llamada por lotes basado por el hecho de “el primero que llega es el primero en ser atendido”, cuando era ejecutado el programa, la computadora tenia uso exclusivo para él. 
El concepto más importante en cualquier sistema operativo es el de proceso, una abstracción de un programa en ejecución, es por ello que todos lo demás depende de este concepto. Los procesos son una de las abstracciones más antiguas e importantes que proporcionan los sistemas operativos, proporcionan la capacidad de operar concurrentemente.
El concepto de múltiples hilos de ejecución aparece con los sistemas de tiempo compartido, donde más de una persona podía conectarse a una computadora central a la vez. Con esto nacen los conceptos de proceso e hilo (thread).  Los primeros sistemas operativos funcionaban con un único hilo de ejecución, por mencionar un ejemplo, DOS. Pero con la llegada actual de aplicaciones gráficas y en red, el uso de los sistemas operativos multiproceso y multihilo se volvieron algo común. Con ello se ha obtenido un mayor uso y rendimiento.
En este presente reporte se hablará sobre los conceptos más relevantes de procesos e hilos, haciendo mención sobre la programación con hilos, funcionamiento de hilos y un pequeño programa en donde se puede apreciar la programación de hilos y el funcionamiento.







2.     Marco teórico

2.1. Proceso


Un proceso se puede definir como un programa en ejecución en la computadora o en algún otro dispositivo que contenga un sistema operativo, compartiendo el microprocesador, la memoria y otros recursos del sistema, con otros programas. El proceso es invocado por un código ejecutable, además, posee una única existencia. Cada proceso se ejecuta de manera aislada los otros, cada proceso se ejecuta de manera aislada a los otros. Los procesos ejecutan módulos de código, los cuales pueden ser independientes o no.

2.1.1. Creación de un proceso


Existen cuatro eventos principales que provocan la creación de procesos:
·        El arranque del sistema.
·        La ejecución, desde un proceso, de una llamada al sistema para creación de procesos.
·        Una petición de usuario para crear un proceso.
·        El inicio de un trabajo por lotes.

2.1.2. Terminación de un proceso


Una vez que se crea un proceso, empieza a ejecutarse y realiza el trabajo al que está destinado. Sin embargo, cada proceso tiene su fin. Tarde o temprano el nuevo proceso terminara, por lo general debido a una de las siguientes condiciones:
·        Salida normal (voluntaria).
·        Salida por error (voluntaria).
·        Error fatal (involuntaria).
·        Eliminado por otro proceso (involuntaria).
La mayoría de los procesos terminan debido a que ha concluido su trabajo.

2.1.3. Estado de un proceso


En la figura 2.1 podemos ver un diagrama de estados que muestra los tres estados en los que se puede encontrar un proceso:

Figura 2.1. Un proceso puede estar en estado de ejecución, bloqueado o listo, con diferentes transiciones entre estos estados. Fuente: Tanenbaum, Andrew s. y. (2009). sistemas operativos modernos.

Hay 4 transiciones posibles como se muestra en la imagen. La transición 1 ocurre cuando el sistema operativo descubre que un proceso no puede continuar justo en ese momento. La transición 2 y 3 dependen del planificador de procesos, la transición 2 ocurre cuando el planificador decide que el proceso en ejecución se ha ejecutado el tiempo suficiente para que a si otro proceso tenga el tiempo de los recursos de la CPU. La transición 3 ocurre cuando todos los procesos han tenido su tiempo de la CPU y es momento de que el primer proceso obtenga la CPU para ejecutarse nuevamente. La transición 4 ocurre cuando se Produce un evento externo.

2.2. Hilos (Threads)


En los sistemas operativos tradicionales, cada proceso tiene un espacio de direcciones y un solo hilo de control. Los hilos de ejecución o threads, surgieron con la necesidad de que existieran aplicaciones que realizaran varias acciones a la vez, así como una mayor libertad en cuanto a seguir una sola línea de ejecución y realizar varias tareas simultaneas. Es más preferible que un programa siga funcionando, ejecutando otras acciones simultaneas. Se puede decir que un hilo es una mínima parte de un proceso o miniprocesos.
Una de las principales razones por la que es necesario utilizar hilos es que en muchas aplicaciones se desarrollan varias actividades a la vez. Una segunda razón para tener hilos es porque son más ligeros que los procesos, son más rápidos de crear y destruir, la creación de hilos es de 10 a 100 veces más rápidas que la de un proceso. Otra razón de tener hilos es el rendimiento. Los hilos no producen un aumento en el rendimiento cuando todos ellos están ligados a la CPU, pero cuando hay una gran cantidad de demanda de E/S, al tener hilos estas actividades se pueden traslapar, con lo cual se agiliza la velocidad de la aplicación.
En la figura 2.2. (a)  se observa tres procesos con un hilo de control, por el lado contrario, en la figura (b) se observa un proceso que ejecuta tres hilos de control.


Figura 2.2. (a) tres procesos son ejecutados cada uno con un hilo de control. (b) un proceso es ejecutado con tres hilos de control. Fuente: Tanenbaum, Andrew s. y. (2009). sistemas operativos modernos.

Cuando un proceso con multihilamiento en sistema con una CPU, los hilos toman turnos para ejecutarse. Los CPU conmutan rápidamente entre un hilo y otro, dando a entender que los hilos se ejecutan en paralelo. Todos los hilos tienen el mismo espacio de direcciones, lo cual significa que también comparten las mismas variables globales. Como cada hilo puede acceder a cada dirección de memoria dentro del espacio de direcciones del proceso, un hilo puede leer, escribir o incluso borrar la pila de otro hilo.
En la figura 2.3 se muestra una comparación que hay entre elementos por procesos y elementos por hilo. Los elementos de la primera columna son propiedades de un proceso, no de un hilo.

Figura 2.3. La primera columna muestra un listado de algunos elementos compartidos por todos los hilos en un proceso. La segunda, algunos elementos que son privados para cada hilo. Fuente: Tanenbaum, Andrew s. y. (2009). sistemas operativos modernos. 

2.3. Programación con threads en linux


Actualmente existen librerías o paquetes de threads que permiten escribir con varias líneas de ejecución, sincronizadas a través de memoria compartida. Sin embargo, la programación de hilos supone nuevas dificultades a comparación de la programación secuencial. 
Un thread es su concepto más básico, puede ser visto como un simple flujo de control secuencial. Con un único punto de ejecución, el programador no necesita aprender nada nuevo para programar un único thread. Sin embargo, cuando se tienen múltiples hilos, significa que el programa tiene distintos puntos de ejecución. Las facilidades que nos ofrecen las librerías de hilos, son conocidas como primitivas ligeras, y existen varios tipos:
·        Creación.
·        Mantenimiento.
·        Sincronización.
·        Destrucción.

2.3.1. Posix


Por sus siglas en ingles Portable Operating System Interface (for Unix). Es un estándar orientado a facilitar la creación de aplicaciones confiables y portables. La mayoría de las versiones populares de UNIX cumplen este estándar en gran medida. La biblioteca para el manejo de hilos en POSIX es Pthread.

2.3.2. Gestión de Hilos


Un paquete de manejo de hilos generalmente incluye funciones para: crear y destruir un hilo, itineración, forzar exclusiones mutuas y espera condicionada. Los hilos de un proceso comparten variables globales, descriptores, de archivos abiertos, y puede cooperar o interferir con otros hilos. Todas las funciones de hilos POSIX comienzan con pthread. Entre ellas están:

Función POSIX
Descripción
pthread_equal
Verifica igualdad de dos identificadores.
pthread_self
Retorna ID del propio hilo.
pthread_create
Crea un hilo.
pthread_exit
Termina el hilo sin terminar el proceso.
pthread_join
Espera por el término de un hilo.
pthread_cancel
Termina otro hilo.
pthread_detach
Configura liberación de recursos cuando termina.
pthread_kill
Envía una señal a un hilo.

Para hacer uso de estas funciones, es necesario incluir la libreria <pthread.h>.

Creación de un hilo (pthread_create)

Esta función automáticamente pone en ejecución el hilo que crea. La sintaxis es la siguiente:
int pthread_create( pthread_t  *thread, const pthread_attr_t  *attr, void  *(*start_routine)(void*), void *arg);

Donde:
  • ·        El parámetro thread apunta al ID del hilo recientemente creado.
  • ·        El parámetro attr representa un objeto atributo que encapsula los atributos de un hilo. Si attr es NULL, el hilo nuevo tendrá los atributos asignados por defecto.
  • ·     El tercer parámetro, start_routine, es el nombre de la función que es invocada por el hilo cuando comienza su ejecución. 
  • ·        El parámetro arg especifica el parámetro que recibe la función start_routine.

Si pthread_create se ejecuta satisfactoriamente retorna 0. Si la rutina no se ejecuta satisfactoriamente retorna un código de error diferente de cero.

Salir (pthread_exit).

Un hilo puede terminar de tres maneras sin terminar el proceso: retornando de su rutina de inicio, cancelado por otro hilo del mismo proceso, o llamando pthread_exit.
void pthread_exit(void *value_ptr);
El valor de value_ptr debe apuntar a datos que existan.

Unir (pthread_join)

La función pthread_join suspende la ejecución del hilo que la invoca hasta que el hilo objetivo, especificado por el primer parámetro, termine.
int pthread_join(pthread_t, void **value_ptr);
El parámetro valor_ptr es un apuntador al valor de retorno que el hilo objetivo pasa a la función pthread_exit o a return. Si value_ptr es NULL, el hilo que invoca join no recibe el valor de retorno del hilo objetivo.
Si la función termina exitosamente retorna 0. Si no termina exitosamente retorna un valor de error diferente de cero.

Referencias a hilos por su id (pthread_self)

Cualquier hilo puede conocer su ID invocando a:
pthread_t pthread_self(void);
Esta función retorna el ID del hilo que la invoca.

Separar (pthread_detach)

Cuando un hilo termina no libera sus recursos al menos que el hilo este separado.  La función pthread_detach modifica las opciones internas del hilo para especificar que el espacio utilizado para el almacenamiento del mismo puede ser reclamado cuando culmine. Los hilos ya separados no reportan su estado cuando culminan.
int pthread_detach( pthread_t thread );
La función pthread_detach recibe un solo parámetro, thread, que es el ID del hilo a ser separado. Si pthread_detach culmina exitosamente retorna uno. Si no culmina exitosamente retorna un valor diferente de cero. La siguiente tabla muestra los códigos de error de pthread_detach.

Cancelar (pthread_cancel)

Un hilo invoca pthread_cancel para solicitar que otro hilo sea cancelado. El resultado de la invocación es determinado por el tipo del hilo objetivo y su estado de cancelación.

int pthread_cancel(pthread_t thread);

El parámetro de pthread_cancel es el ID del hilo objetivo a ser cancelado. Si pthread_cancel se ejecuta exitosamente retorna 0. Sino retorna un valor diferente de cero.

2.3.3. Mutex en Pthreads


Pthreads proporciona varias funciones que se pueden utilizar para sincronizar los hilos. El mecanismo básico utiliza una variable mutex, cerrada o abierta, para resguardar cada región crítica. Un hilo que desea entrar a una región crítica primero trata de cerrar el mutex asociado. Si el mutex está abierto, el hilo puede entrar de inmediato y el bloqueo se establece en forma automática, evitando que otros hilos entren; si el mutex ya se encuentra cerrado, el hilo que hizo la llamada se bloquea hasta que el mutex esté abierto. Si hay varios hilos esperando el mismo mutex, cuando está abierto sólo uno de ellos podrá continuar y volver a cerrarlo. Estos bloqueos no son obligatorios. Es responsabilidad del programador asegurar que los hilos los utilicen de manera correcta.







3.     Desarrollo


3.1. Requerimientos


Para el desarrollo de la práctica, se realiza en un ambiente Linux, en este caso se realiza la practica con la distribución OpenSuse 12.2. Teniendo el sistema operativo Linux, se descarga el compilador gcc. Por ello será necesario ejecutar en la terminal el siguiente comando:
Sudo zypper install gcc.
Una vez instalado se procede a corroborar que efectivamente el compilador gcc se ha instalado. Es por ello que es necesario ejecutar en la terminal el siguiente comando:
gcc –v
Este comando muestra la versión instalada en el sistema operativo. En la figura 3.1. muestra la ejecución del comando.

Figura 3.1. Se observa la versión del compilador gcc en este caso se cuenta con la versión 4.7.1.

3.2. Ejecución del programa.


Se cuenta con un programa desarrollado en el lenguaje de programación C, en donde se muestra el funcionamiento de threads. La ejecución de este programa se comporta de la siguiente manera: Se crean varios hilos a la vez y al mismo tiempo comienza con su ejecución.
En la figura 3.2 se muestra el código en general, más adelante se explica dicho código.
 
 Figura 3.2. Se observa el código en general que se utilizó para la práctica.

Para ejecutar el programa, es necesario abrir la terminal y ubicarse en la carpeta en donde se encuentra el programa, en este caso se accedió a la carpeta de documentos con el siguiente comando:
cd Documentos/
Hecho lo anterior, se procede a compilar el programa con el siguiente comando:
gcc main.c –o hilo –l pthread
En la figura 3.3 se observa la terminal con el comando tecleado para compilar el programa. Cabe señalar que se invoca la librería phtread.

 Figura 3.3. Comando para compilar el programa.

Como ya no existe errores de sintaxis, se crea el archivo ejecutable hilo, solo queda ejecutarlo con el siguiente comando:
./hilo
En la figura 3.4. se muestra la ejecución del programa.

 







Figura 3.4. Se observa el programa ejecutado, se crean varios hilos a la vez y al mismo tiempo comienza con su ejecución.

3.3. Analizando el código


En primer lugar, se invocan las librerías necesarias a utilizar, también se define una variable estática en donde contendrá el número de hilos a crear durante el programa.
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS     5
A continuación, se implementa la función que realizan los hilos definidos, esta función realiza la impresión del hilo que se encuentra ejecutando en este momento. Recibe como parámetro el número de hilo, pero como apuntador, por lo que se hace un cast para obtener su valor original.
// La función que es utilizada en un Hilo
// deberá de retornar un puntero sin tipo
void *imprimeSaludo(void *threadid){
   long tid;
   // Aquí se castea lo que le fue pasado
   // al hilo como atributos
   tid = (long)threadid;
   printf("Se esta ejecutando el hilo %ld \n", tid);  
   pthread_exit(NULL); }   //Se finaliza la ejecución del Hilo
Por último, dentro del main, se crea la variable que creara los hilos, tomando como longitud el número de la variable especificada anteriormente, creando cada hilo en un for, llamando a su ejecución al mismo tiempo.
int main ()
{
   pthread_t threads[NUM_THREADS]; //Direccion donde se almacenarán los hilos (5 en total)
   int rc;
   int t;
   for(t=0; t < NUM_THREADS; t++){
      printf("Creando Hilo %ld \n", t);
      rc = pthread_create(&threads[t], NULL, imprimeSaludo, (void *)t);
      if (rc){
         printf("ERROR; return code from pthread_create() is %d \n", rc);
         exit(-1);
      }
      //pthread_join(threads[t],NULL);   //Se espera a que finalice el hilo
   }
   pthread_exit(NULL);      //Se finaliza la ejecución del Hilo
}
En la función rc = pthread_create(&threads[t], NULL, imprimeSaludo, (void *); se utiliza una variable de tipo entero, la cual obtendrá un valor que determinara si el hilo fue creado correctamente, en caso de que devuelva un 1 el hilo no se creó correctamente y pasara a informar el error.

Los parámetros que recibe son:
·        El parámetro &trheads[t] apunta al ID del hilo recientemente creado.
·        El parámetro attr representa un objeto atributo que encapsula los atributos de un hilo. En este caso es NULL, el hilo nuevo tendrá los atributos asignados por defecto.
·        El tercer parámetro, imprimeSaludo, es el nombre de la función que es invocada por el hilo cuando comienza su ejecución. 
·        El parámetro void* especifica el parámetro que recibe la función start_routine.
Como parte final, se corrobora que en la librería <pthread.h>, se encuentra todas las funciones que existen y se utilizaron en el programa, así como su calendarización que utilizan los hilos. La localización de la librería se encuentra en:
[1] /usr/include.[2] 
En la figura 3.5 se observa la librería pthread.h que fue ocupada en el código.








Figura 3.5. Se observa la librería ocupada







4.     Artículo  


Implementación de hilos-POSIX en paralelización y evaluación del rendimiento paralelo en el nivel macro bloque de un decodificadorH.264 en una arquitectura multiprocesador ¬NUMA.
En este artículo de Redalyc se toma principal mente la referencia en la utilización e implementación de la programación en hilos, como trabajo primordial de este artículo es que se ha investigado la paralelización y evaluación del rendimiento paralelo en el nivel macro bloque de un decodificadorH.264 en una arquitectura multiprocesador ¬NUMA haciendo uso de ello.
Se menciona que se ha implementado una paralelización en una arquitectura multiprocesador. Tal que la mejor estrategia de programación es la combinación de programación dinámica con tailsubmit. Y hace referencia que la programación dinámica se ocupa del problema de tiempo de decodificación variable al asignar MBs a los procesadores de igual forma ya que han resuelto todas las dependencias. Pero debido al uso dinámico, la programación sufre una gran sobrecarga de contención en una cola de tareas centralizada e ineficiencias en el hilo API de sincronización. Mediante el uso de la transmisión, el número de subprocesos que el acceso a la cola de tareas se reduce la mayoría de los MB son procesado directamente por los hilos de trabajo sin requerir ningún Sincronización adicional.
Además, de que explota localidad de datos reduciendo la memoria externa en presión. Y de que hay una sobrecarga considerable de hilo sincronización que proviene del muy bajo rendimiento de los POSIX, semáforos en tiempo real en la arquitectura evaluada. El uso de mecanismos de sincronización alternativos como el bloqueo algoritmos libres u otra sincronización compatible con hardware mecanismos pueden ayudar a aumentar la eficiencia de la paralelización
Se menciona que la posible solución, es que la arquitectura, incluye soporte de hardware diseñado específicamente para hilo en sincronización y programación. Al hacer eso, es posible eliminar la sobrecarga de sincronización y aumento de subprocesos, y eficiencia de la paralelización.
Y que otra limitación importante de la escalabilidad de paralelización, es el rendimiento de la memoria en una arquitectura NUMA sin accesos predecibles, como los realizados en el movimiento etapa de compensación, resulta en accesos de descarga que tienen una alta latencia. En una arquitectura multinúcleo esto puedo definirse como una gran demanda de ancho de banda para la memoria externa. Esto es un problema abierto y se menciona que están trabajando en optimizaciones de hardware estrategias para aumentar la localidad de los accesos de datos.

5.     Conclusión  


En dicha práctica se hace mención acerca de los hilos, ya que los hilos son generados a partir de la creación de un proceso, podemos decir que un proceso es un hilo de ejecución.
Hablar de hilos tienen muchas ventajas ya que también pueden ser mejor conocidos como Multihilos, que es cuando un proceso tiene múltiples hilos de ejecución los cuales realizan actividades distintas, que pueden o no ser cooperativas entre sí. Los beneficios de los hilos se derivan de las implicaciones de rendimiento.
Al igual que los procesos, los hilos poseen un estado de ejecución y pueden sincronizarse entre ellos para evitar problemas de compartición de recursos. Generalmente, cada hilo tiene una tarea específica y determinada, como forma de aumentar la eficiencia del uso del procesador.

6.     Bibliografía


Tanenbaum, Andrew s. y. (2009). sistemas operativos modernos. México: Pearson educación.
Abraham Silberschatz. (2011). Operating System Concepts. 2012, de Operating System Sitio web: http://iips.icci.edu.iq/images/exam/Abraham-Silberschatz-Operating-System-Concepts---9th2012.12.pdf
Claudia. (lunes, 19 de septiembre de 2011). sistemas operativos. Recuperado el 10 de febrero de 2018, de sistemas operativos: http://claus-sistemasoperetivos.blogspot.mx/2011/09/identificador-de-procesos.html
Alvarez, Mauricio; Ramirez, Alex; Valero, Mateo; Azevedo, Arnaldo; Meenderinck, Cor; Juurlink, Ben. (1, junio, 2009). Evaluación del rendimiento paralelo en el nivel macro bloque del decodificador H.264 en una arquitectura multiprocesador ccNUMA. Revista Avances en Sistemas e Informática, vol. 6, pp. 219-228.





 [1]
 [2]

No hay comentarios:

Publicar un comentario