En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

viernes, 25 de agosto de 2017

Thread Ringers
cómo usar los thread en C - pt.1

Los thread son un poco como los gemelos de la obra maestra de David Cronenberg: tienen el mismo origen, parecen iguales pero son diferentes.
...te lo explicaré: yo soy el thread A y tu eres el B...
En este post (que es el primero de una serie corta) veremos un ejemplo muy simple de cómo usar los thread en C: obviamente el tema es vasto y se puede hacer mas complicado a voluntad, pero nuestro ejemplo ya contiene los conceptos básicos para entender cómo funciona todo, o sea: la creación, la sincronización y la destrucción de los thread. Obviamente empezaremos usando la versión de base (casi) universal, es decir, usaremos los POSIX Threads. Y ahora vamos al grano, ¡vamos con el código!

#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.