Introducció als Sistemes Operatius SESSIÓN 6: Concurrencia Los objectivos de esta sesión son: • Reforzar el concepto de thread (flujo) y relacionarlo con el de proceso. • Conocer los mecanismos básicos de sincronización y comunicación entre threads. • Utilizar una librería para poner en práctica la programación concurrente de threads. LIBRERÍA FLUXES Se presenta a continuación un interfaz de threads de usuario construido sobre los PThreads de OSF/1 (sistema operativo de alabi/alaba). El motivo de no utilizar directamente la librería de PThreads ha sido el facilitar y simplificar las llamadas, reduciendo el número de argumentos que se ha de pasar a las funciones. A todos los efectos el funcionamiento es equivalente y las llamadas muy similares. Esta librería de threads se realizó en el año 1994 por los profesores y becarios de la asignatura en aquellos momentos. NOTA: si ejecutas la orden ~/softiso/bin/mkmanpath podrás acceder a las páginas de manual de todas las rutinas de la librería en el formato habitual de la utilidad man. Por ejemplo: man crear_fluxe Control de threads #include <fluxes.h> fluxe_t crear_fluxe( funcio_t nom_funcio, long argument); PARÁMETROS • nom_funcio Nombre de la rutina que ejecutará el thread creado. • argument Argumento que recibirá la rutina. DESCRIPCIÓN Crea un nuevo thread de ejecución en el proceso que la invoca. Este thread iniciará su ejecución llamando a la rutina nom_funcio, pasándole como único argumento argument. Éste tiene un tamaño de 8 bits (tipo long) y puede utilizarse para codificar un entero o un puntero, por ejemplo. VALORES DE RETORNO Devuelve el identificador del thread creado (necesario si se quiere sincronizar con su finalización con la llamada esperar_fluxe()). tid = crear_fluxe(empezar); . . . esperar_fluxe(tid,&res); se crea un nuevo thread cuyo código es empezar() void empezar () fi_fluxe(res); sincroniza su finalización con el thread padre (puede ser cualquier thread de la aplicación que especifique el identificador de thread concreto) 1 Introducció als Sistemes Operatius #include <fluxes.h> int esperar_fluxe( fluxe_t flux, int *estat); PARÁMETROS • flux Identificador del thread a esperar. • estat Variable donde se guarda el código de finalización del thread flux. DESCRIPCIÓN Espera la finalización del thread indicado en el parámetro flux y permite conocer cómo ha ido su ejecución, a través del código de finalización que se le pasa. VALORES DE RETORNO Devuelve 0 si todo escorrecto. Si es distinto de 0 es que ha habido un error (el identificador del thread no es válido o se ha producido un abrazo mortal a nivel de pthreads). #include <fluxes.h> void fi_fluxe( int estat); PARÁMETROS • estat Código de finalización del thread. DESCRIPCIÓN Provoca la destrucción del thread. El parámetro estat lo recibe el thread que haga la llamada a esperar_fluxe(). Si es el último thread del proceso, provoca una llamada a exit(). VALORES DE RETORNO No tiene. La destrucción del thread siempre se cumple. #include <fluxes.h> fluxe_t quisoc( void ); PARÁMETROS Ninguno. DESCRIPCIÓN Devuelve el identificador del thread que hace la llamada. VALORES DE RETORNO El identificador del thread actual. Sincronización entre threads Mecanismo de mutex 2 Introducció als Sistemes Operatius #include <fluxes.h> mutex_id crear_mutex(void); PARÁMETROS Ninguno. DESCRIPCIÓN Crea un semáforo de exclusión mutua inicializado a 1. VALORES DE RETORNO Identificador del semáforo. #include <fluxes.h> int destruir_mutex(mutex_id mutex); PARÁMETROS • mutex Identificador del semáforo que se quiere destruir. DESCRIPCIÓN Destruye el semáforo de exclusión mutua pasado como parámetro. Este valor ha de ser el que devolvió la llamada crear_mutex() cuando se creó. VALORES DE RETORNO Devuelve 0 si todo es correcto. Si el valor que devuelve es diferente de 0, indica que el identificador del semáforo no es válido o el semáforo está bloqueado. #include <fluxes.h> int mutex_lock(mutex_id mutex); PARÁMETROS • mutex Identificador del semáforo que controla la sección crítica a la que se quiere acceder. DESCRIPCIÓN Pide acceso en exclusión mutua a la sección crítica que controla el semáforo mutex pasado como parámetro. Si el semáforo está cerrado (ya hay alguien en la sección crítica) el thread que hace la llamada se queda bloqueado. La cola de threads bloqueados es de tipo FIFO. VALORES DE RETORNO Devuelve 0 cuando se accede a la sección crítica, tanto si ha habido bloqueo como si no. Si el valor que devuelve es diferente de 0, indica que el identificador del semáforo no es válido o el semáforo está bloqueado. #include <fluxes.h> int mutex_unlock(mutex_id mutex); PARÁMETROS • mutex Identificador del semáforo que controla la sección crítica de la que se quiere salir. 3 Introducció als Sistemes Operatius DESCRIPCIÓN Notifica el abandono de la sección crítica que controla el semáforo mutex pasado como parámetro. Permite que un thread que quiera acceder, pueda hacerlo. Si hay threads bloqueados en la cola del semáforo, se desbloqueará al primero de ellos. VALORES DE RETORNO Devuelve 0 si el semáforo existe. En otro caso, devuelve un valos distinto de 0. Semáforos #include <fluxes.h> sem_id crear_sem(int compt_ini); PARÁMETROS • compt_ini Valor inicial del semáforo n-ario. DESCRIPCIÓN Crea un semáforo n-ario e inicializa su contador con el valor indicado. VALORES DE RETORNO Identificador del semáforo n-ario creado. #include <fluxes.h> void sem_init(sem_id sem, int compt_ini); PARÁMETROS • sem Identificador del semáforo n-ario • compt_ini Valor inicial del semáforo n-ario. DESCRIPCIÓN Crea un semáforo n-ario e inicializa su contador con el valor indicado. Devuelve en sem el identificador del semáforo. VALORES DE RETORNO No tiene. #include <fluxes.h> int destruir_sem (sem_id semafor); PARÁMETROS • semafor Identificador del semáforo que se quiere destruir. DESCRIPCIÓN Destruye el semáforo n-ario pasado como parámetro. Este valor ha de coincidir con el que devolvió la llamada crear_sem() o sem_init(). 4 Introducció als Sistemes Operatius VALORES DE RETORNO No definido. Si el semáforo no existe, puede tener consecuencias imprevistas. #include <fluxes.h> int sem_signal(sem_id semafor); PARÁMETROS • semafor Identificador del semáforo sobre el que se hace sem_signal(). DESCRIPCIÓN Incrementa el contador interno del semáforo n-ario que se indica como parámetro. Si había algún thread bloqueado en la cola del semáforo, desbloquea al primero (cola FIFO). VALORES DE RETORNO No tiene ninguno definido, aunque el hecho de hacer sem_signal() sobre un semáforo inexistente puede tener consecuencias imprevistas. #include <fluxes.h> int sem_wait(sem_id semafor); PARÁMETROS • semafor Identificador del semáforo sobre el que se hace sem_wait(). DESCRIPCIÓN Decrementa el contador interno del semáforo n-ario que se indica como parámetro. Si el contador es negativo, el thread que hace la llamada se queda bloqueado en la cola del semáforo (tipo FIFO). VALORES DE RETORNO No tiene ninguno definido, aunque el hecho de hacer sem_wait() sobre un semáforo inexistente puede tener consecuencias imprevistas. Espera activa #include <fluxes.h> int test_and_set(int *addr); PARÁMETROS • addr Dirección de memoria sobre la que se hace test_and_set(). DESCRIPCIÓN Hace un test and set sobre la dirección de memoria addr. De manera atómica, devuelve el contenido de la dirección addr y actualiza ese contenido a 1. Sirve para construir protocolos de entrada/ salida en secciones críticas a las que haya que acceder en exclusión mutua. Por ejemplo, si se inicializa el contenido de *addr a 0, sólo el primer thread que acceda leerá un 0: el resto, leerá el 1 que deja la función test_and_set() y dejarán a su vez un 1. Al salir de la exclusión mutua, habrá que volver a cargar *addr con 0 para que algún thread de los que está consultando el valor lo coja y entre a su vez. Son protocolos de espera activa y por tanto no se garantiza el orden de entrada de 5 Introducció als Sistemes Operatius los threads que lo intentan. VALORES DE RETORNO Devuelve el valor anterior de *addr. El código de los ejemplos que vienen a continuación lo podéis encontrar en el directorio de la asignatura ~iso/softiso/practiques/ sessio6 EJEMPLO 1 (3flu.c) Queremos implementar el cálculo de la siguiente función mediante threads: f(x) = 2(3x + 4) Por la línea de comandos se pasa como parámetro el valor x al que se quiere aplicar la función f(), y el programa principal tiene que escribir el resultado por la salida estándar. Utilizaremos tres threads para calcular la función: el primero, calculará el producto 3x; el segundo sumará 4 al resultado del thread anterior y el tercero multiplicará por 2 el resultado anterior. El thread principal (el que ejecuta el programa principal e inicia la ejecución) creará los tres threads al inicio y esperará por su finalización. NOTA: Recordad el Principio Básico de Concurrencia -no se puede predecir nada sobre la velocidad de ejecución de varios threads concurrentes- a la hora de esperar resultados y finalizaciones entre threads. Utilizaremos una variable global (num) que irá acumulando el resultado del cálculo y dos semáforos de sincronización (por tanto, inicializados a cero): uno para que el segundo thread espere hasta que el primero le avise de que ha finalizado su cálculo (sync1_2), y el otro para que el tercer thread espere el aviso de fin de cálculo del segundo (sync2_3). El esquema para que la actualización de la variable num sea correcto, se puede ver a continuación. Fijaos que los semáforos permiten que se ordene el momento de acceso a la variable. Una vez llamada a la función crear_fluxe(), ya tenemos la ejecución concurrente del thread principal (la continuación del código a la vuelta de dicha llamada) y del fluxe (thread) que se acaba de crear (la función que se le indica como parámetro). Y así sucesivamente conforme se van creando fluxes. 6 Introducció als Sistemes Operatius /*padre*/ main() f1 = crear_fluxe(flux1, (long)0); f2 = crear_fluxe(flux2,(long)0); f3 = crear_fluxe(flux3,(long)0); /*flujo 1*/ void *flux1(void *par) { num=num*3 sem_signal(sync1_2) } 1 /*flujo 2*/ void *flux2(void *par) { sem_wait(sync1_2) num=num+4 sem_signal(sync2_3) /*flujo 3*/ void *flux3(void *par) { sem_wait(sync2_3) num=num*2 } } 2 3 f1, f2,f3 num sync1_2, sync2_3 datos en memoria global (visibles por todos los flujos) Esta es una posible solución: #include <fluxes.h> #include “error.h” int num; sem_id sync1_2, sync2_3; void *flux1(void *par) { num = num * 3; sem_signal(sync1_2); fi_fluxe(0); } void *flux2(void *par) { sem_wait(sync1_2); num = num + 4; sem_signal(sync2_3); fi_fluxe(0); } 7 Introducció als Sistemes Operatius void *flux3(void *par) { char s[50]; sem_wait(sync2_3); num = num * 2; sprintf(s,”El resultat es %d\n”, num); write(1, s, strlen(s)); fi_fluxe(0); } main(int argc, char *argv[]) { int foo; fluxe_t f1, f2, f3; if (argc!=2) error(“Necessita un unic parametre\n”, PROPI); num = atoi(argv[1]); sem_init(sync1_2, 0); sem_init(sync2_3, 0); f1 = crear_fluxe(flux1, (long)0); f2 = crear_fluxe(flux2, (long)0); f3 = crear_fluxe(flux3, (long)0); esperar_fluxe(f1, &foo); esperar_fluxe(f2, &foo); esperar_fluxe(f3, &foo); } NOTA: el formato de las cabeceras de las rutinas flux1(), flux2() i flux3() viene dado por la librería de fluxes. Algunas cuestiones: • ¿Por qué no se ha utilizado ningún semáforo de exclusión mutua si los tres threads están modificando una variable global (num)? • Aprovechando la comunicación entre las llamadas fi_fluxe() y esperar_fluxe(), modifica el programa para que el thread principal escriba los resultados intermedios de flux1(), flux2() y flux3(). • Queremos tratar el caso de que nos pasen más de un valor por la línea de comandos para evaluar la función. ¿Qué se tendría que añadir a la solución dada para tratar este caso? (Se ha de hacer sin más flujos). 8 Introducció als Sistemes Operatius EJEMPLO 2 (prod_con.c) Queremos implementar una solución al modelo de productores/consumidores en un caso en el que tenemos un buffer circular de tamaño N que contiene números. Los productores generan y escriben números en el buffer mientras no esté lleno. Los consumidores procesan los números del buffer mientras no esté vacío: ... e1 e2 e3 0 N-1 in void *productor(void *n) { int producte; int i; out for (i=0;i<ELEM;i++) { producte=i+10*(int)n; BUFFER[in]=producte; in=(in+1)%N; } void *consumidor(void *n) { int producte; int i; for (i=0;i<ELEM;i++) { producte=BUFFER[out]; out=(out+1)%N; } } } La solución que proponemos utiliza dos semáforos generales y un ‘mutex’ que, en realidad, es un semáforo de exclusión mutua. Con esta exclusión mutua, los productores y los consumidores acceden de forma exclusiva a los punteros del buffer circular (in, out). e1 e2 e3 ... 0 N-1 out out cons1 cons2 consn V acceso en mutex mutex_lock(mutex) in prod1 prod2 prodn V acceso en mutex mutex_lock(mutex) Fíjate que, como las zonas de trabajo no se relacionan, una mejor solución hubiera sido tener dos exclusiones mutuas: una para el acceso al puntero out y otra para el puntero in. Los semáforos generales se utilizan como semáforos sobre recursos y actúan de filtro: al inicio tenemos N posiciones libres en el buffer (recurso para los productores) y 0 posiciones ocupadas (recurso para los consumidores). Inicializando con estos valores cada uno de los semáforos, aseguramos que un fluxe consumidor se bloqueará cuando el buffer esté vacío y que un fluxe productor se bloqueará cuando el buffer esté lleno. El 9 Introducció als Sistemes Operatius fluxe consumidor se desbloqueará cuando un productor añada un elemento al buffer , y el fluxe productor se desbloqueará cuando un consumidor libere una posición. e1 e2 e3 ... 0 N-1 out in hay datos: sem_signal (ocupades) hay espacio: sem_signal (buides) #include <stdio.h> #include <fluxes.h> #include “error.h” #define N 4 #define CONS 2 #define PRODS 2 #define ELEMENTS 5 int BUFFER[N]; int in=0; int out=0; sem_id buides; sem_id ocupades; mutex_id mutex; void *productor(void * n) { char buffer[100]; int producte; int i; sprintf(buffer, “Comencant a produir fluxe %d ...\n”, (int) n); write (1, buffer, strlen(buffer)); for (i=0; i<ELEMENTS ; i++) { producte=i+10*(int)n; sem_wait(buides); mutex_lock(mutex); BUFFER[in]=producte; in=(in+1)%N; sprintf(buffer, “Productor %d: Generat num. %d\n”, (int)n, producte); write (1, buffer, strlen(buffer)); mutex_unlock(mutex); sem_signal(ocupades); } fi_fluxe(0); } void *consumidor(void * n) { char buffer[100]; int producte; 10 Introducció als Sistemes Operatius int i; sprintf(buffer, “Comencant a consumir fluxe %d ...\n”, (int) n); write (1, buffer, strlen(buffer)); for (i=0; i<ELEMENTS ; i++) { sem_wait(ocupades); mutex_lock(mutex); producte=BUFFER[out]; out=(out+1)%N; sprintf(buffer, “Consumidor %d: Consumit num. %d\n”, (int)n,producte); write (1, buffer, strlen(buffer)); mutex_unlock(mutex); sem_signal(buides); } fi_fluxe(0); } main() { fluxe_t cons[CONS]; fluxe_t prods[PRODS]; int i, res; if ((buides=crear_sem(N))<(sem_id)0) error(“Error buides\n”, SISTEMA); if ((ocupades=crear_sem(0))<(sem_id)0) error(“Error ocupades\n”, SISTEMA); mutex=crear_mutex(); for(i=0;i<PRODS;i++) prods[i]=crear_fluxe(productor, (long)i); for(i=0;i<CONS;i++) cons[i]=crear_fluxe(consumidor,(long)i); for(i=0;i<PRODS;i++) esperar_fluxe(prods[i], &res); for(i=0;i<CONS;i++) esperar_fluxe(cons[i], &res); } Ejecútalo varias veces y comprueba que los mensajes aparecen en el orden esperado. Modifica las constantes del programa (N, CONS, PRODS, ELEMENTS) para probar otras situaciones. Algunas cuestiones: • Propón una forma más eficiente de gestionar el acceso a los punteros in y out por parte de los productores y de los consumidores. • Sustituye las llamadas mutex_lock()y mutex_unlock() por otras de la librería. • ¿Utilizarías esperas activas en lugar de semáforos en algún sitio? ¿Por qué? • ¿Es correcto el funcionamiento si cambiamos el código del bucle principal del consumidor por: for(;;) { sem_wait(ocupades); mutex_lock (mutex); aux=out; out=(out+1)%N; mutex_unlock(mutex); producte=BUFFER[aux]; sem_signal(buides); } for (;;) { sem_wait(ocupades); mutex_lock(mutex); aux=out; out=(out+1)%N; mutex_unlock(mutex); sem_signal(buides); producte=BUFFER[aux]; } 11