...te lo explicaré: yo soy el thread A y tu eres el B... |
#include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> // creo un nuevo tipo parar pasar datos a los thread typedef struct _tdata { int index; // thread index int *comdata; // dato común a los thread pthread_mutex_t *lock; // mutex común a los thread } tdata; // prototipos locales void* tMyThread(void *arg); // función main() int main(int argc, char* argv[]) { int error; // init mutex pthread_mutex_t lock; if ((error = pthread_mutex_init(&lock, NULL)) != 0) { printf("%s: no puedo crear el mutex (%s)\n", argv[0], strerror(error)); return 1; } // init threads pthread_t tid[2]; tdata data[2]; int comdata = 0; for (int i = 0; i < 2; i++) { // set data del thread y crea el thread data[i].index = i; data[i].comdata = &comdata; data[i].lock = &lock; if ((error = pthread_create(&tid[i], NULL, &tMyThread, (void *)&data[i])) != 0) printf("%s: no puedo crear el thread %d (%s)\n", argv[0], i, strerror(error)); } // join threads y borra mutex pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&lock); // exit printf("%s: thread acabados: comdata=%d\n", argv[0], comdata); return 0; } // thread routine void* tMyThread(void *arg) { // obtengo los datos del thread con un cast (tdata*) de (void*) arg tdata *data = (tdata *)arg; // thread loop printf("thread %d comenzado\n", data->index); int i = 0; for (;;) { // lock mutex pthread_mutex_lock(data->lock); // incrementa comdata (*data->comdata)++; // unlock mutex pthread_mutex_unlock(data->lock); // test counter parar eventual salida del loop if (++i >= 100) { // sale del loop break; } // thread sleep (10 ms) usleep(10000); } // el thread sale printf("thread %d acabado\n", data->index); return NULL; }
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales. Suponiendo que ya sabeis lo que son y para qué sirven los thread (si no leer algunas guías introductorias, hay algunas muy buenas en la red) el flujo de código es obvio: primero hay que crear un mutex (con pthread_mutex_init()) para sincronizar los thread que vamos a utilizar, entonces hay que inicializar los datos que hay que pasar a los thread y crear (con pthread_create()) los dos thread de nuestro ejemplo (init de datos y creación los he puesto en un loop de 2, pero también se hubiera podido escribir en dos pasos, obviamente). Finalmente el main() se pone a la espera (con pthread_join()) de la terminación de los thread y, cuando terminan, destruye el mutex (con pthread_mutex_destroy()) y sale.
Como se puede ver pthread_create() tiene cuatro parámetros, que son (en el orden): un pointer a un thread descriptor (que identifica de forma exclusiva el thread creado), un pointer a un contenedor de atributos del thread a crear, un function pointer a la función que ejecutará el thread y, finalmente, un pointer al único argumento que se puede pasar a la función anterior. Específicamente, en nuestro ejemplo (muy simple), he usado los atributos por defecto (usando NULL para el segundo parámetro), y he creado (con typedef) un nuevo tipo ad-hoc para pasar múltiples parámetros a la función que ejecutará el thread, explotando el hecho de que el argumento de función por defecto es un void* que puede ser fácilmente transformado (con una operación de cast) a cualquier tipo complejo (en nuestro caso el nuevo tipo tdata).
En este ejemplo, los dos thread creados ejecuta la misma función, tMyThread() (pero también podrían realizar dos funciones completamente diferentes: en este caso, por supuesto, hubiera tenido que escribir una tMyThread1() y una tMyThread2 ()). El flujo de la función es muy simple: primero ejecuta un cast sobre el argumento arg para utilizar los datos del tipo tdata, luego entra en un clásico thread-loop infinito con salida forzada: en nuestro caso sale cuando el índice i alcanza los 100, pero en un caso real se podría forzar la salida sólo en caso de error, por ejemplo. Tener en cuenta que el thread-loop utiliza una sleep de 10 ms (usando usleep()): ¡intentad olvidar de poner la sleep en un thread-loop realmente infinito y ya veréis los saltos de alegría que hará la CPU del vuestro PC!
Como se puede ver, el tipo tdata contiene un índice típico del thread (en nuestro caso es 0 o 1) y los pointer a los dos datos comunes (locales al main()) que son comdata y lock. Entonces ¿qué hace el thread-loop? Puesto que es un ejemplo simple, sólo incrementa el dato comune comdata inicializado en el main() y lo hace de forma síncronizada utilizando pthread_mutex_lock() y pthread_mutex_unlock() sobre el mutex común lock: esto sirve para evitar que los dos thread accedan al mismo tiempo a comdata.
Compilando con GCC en una máquina Linux (por supuesto) y ejecutando, el resultado es:
aldo@ao-linux-nb:~/blogtest$ gcc thread.c -o thread -pthread aldo@ao-linux-nb:~/blogtest$ ./thread thread 0 comenzado thread 1 comenzado thread 1 acabado thread 0 acabado ./thread: thread acabados: comdata=200
Que es el resultado esperado. En el próximo post hablaremos de una interfaz alternativa a los POSIX Threads. Y, como siempre, os recomiendo que no contengáis la respiración esperando...
¡Hasta el próximo post!
P.D.
Como bien sabéis, este es un blog de programación con un alma cinéfila, así que os comento (con gran tristeza) que el mes pasado nos ha dejado un gran maestro. D.E.P., George.