A la hora de crear un programa concurrente con las hebras definidas en el ejemplo anterior, el modelo de programación puede mostrarse insuficiente. Algunas de las limitaciones que tiene el uso puro de una estrategia de hilos son las siguentes:
Para devolver datos, la hebra tiene que finalizar.
No hay forma de compartir datos de forma que los hilos puedan realizar operaciones de lectura o escritura de forma segura.
Para resolver ese problema se provee un mecanismo llamado cerrojo: pthread_mutex
que permite realizar las siguientes operaciones:
Cerrar un cerrojo con una operación atómica (lock
) que permite
bloquear el cerrojo. Todos los hilos que intenten bloquear el cerrojo a partir de ese instante fracasarán
y se bloquearán.
Liberar el cerrojo con otra operación atómica (unlock
) que libera el cerrojo y que permite que otro hilo lo use.
El resto de la sección cubre estos puntos de forma práctica e ilustrada.
Pensemos en crear un programa con dos hilos que aumentan el valor de una variable global. Eso se puede hacer más fácilmente si pasamos esa variable a los dos hilos y van modificando al mismo tiempo. El código que tendríamos sería el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // compile with $ gcc -Wall -g *.c -pthread -o program // run with ./program // check with valgrind --tool=helgrind ./program #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* thread_run(void* data) { int i_data; sleep(2); printf("[TH_ID:%ld]: Hello from the thread \n", pthread_self()); printf("[TH_ID:%ld]: Reading %i \n", pthread_self(),(*(int*)data)); i_data=(*(int*)data); sleep(1); i_data++; (*(int*)data)=i_data; printf("[TH_ID:%ld]: Writing %i \n", pthread_self(),(*(int*)data)); printf("[TH_ID: %ld]: To exit...............\n",pthread_self()); return data; } int main() { int i; pthread_t thread[2]; int data=0; int thread_rc; for (i=0;i<2; i++) { printf("[MAIN:%ld]: Starting............ \n",pthread_self()); if ((thread_rc=pthread_create(&thread[i],NULL,thread_run,&data))!=0) { printf("Error creating the thread. Code %i",thread_rc); return -1; } } sleep(1); printf("[MAIN:%ld]: Thread allocated \n",pthread_self()); int *ptr_output_data; for ( i=0;i<2; i++) { pthread_join(thread[i],(void **)&ptr_output_data); } printf("[MAIN:%ld]: Thread returns %d \n",pthread_self(), *ptr_output_data); return 0; } |
Lo que haría que se generase la siguiente traza de ejecución errónea:
[MAIN:2]: Starting............ [MAIN:2]: Starting............ [MAIN:2]: Thread allocated [TH_ID:0]: Hello from the thread [TH_ID:0]: Reading 0 [TH_ID:6]: Hello from the thread [TH_ID:6]: Reading 0 [TH_ID:6]: Writing 1 [TH_ID:6]: To exit............... [TH_ID:40]: Writing 1 [TH_ID:0]: To exit............... [MAIN:2]: Thread returns 1
El problema que presenta el código es que la operación de lectura, modificación y escritura de un resultado no puede realizarse concurrentemente. Si dos hilos intentan leer y escribir al mismo tiempo pueden darse potencialmente las siguientes ejecuciones "erróneas" (todas ellas devuelven 1 en vez de 2):
H_0 lee(0), H_6 lee(0), H_0 esc(1), H_6 esc(1)
H_6 lee(0), H_0 lee(0), H_0 esc(1), H_6 esc(1)
H_0 lee(0), H_6 lee(0), H_6 esc(1), H_0 esc(1)
H_6 lee(0), H_0 lee(0), H_6 esc(1), H_0 esc(1)
El programa también tiene las siguientes ejecuciones correctas, que podrían ser también obtenidas dependiendo de cómo lo ejecute la infraestructura:
H_0 lee(0), H_0 esc(1), H_6 lee(1), H_6 esc(2)
H_6 lee(0), H_6 esc(1), H_0 lee(1), H_0 esc(2)
Este mal comportamiento se llama condición de carrera (race condition en inglés).
Un phtread_muxtex_unlock
tiene el siguiente comportamiento sobre el sistema:
Si en el cerrojo mutex
hay otros hilos esperando se le cede el paso a uno de ellos (que estaba bloqueado en el mismo mutex
).