SESSIÓN 6: Concurrencia

Anuncio
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
Descargar