Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

giovedì 24 agosto 2017

Thread Ringers
come usare i thread in C - pt.1

I thread sono un po' come i gemelli del capolavoro del grande David Cronenberg: hanno la stessa origine, sembrano uguali ma sono diversi.
...ti spiego: io sono il thread A e tu sei il B...
In questo post (che è il primo di una breve serie) vedremo un esempio semplice semplice di come usare i thread in C: ovviamente l'argomento è molto vasto e complicabile a piacere, ma il nostro esempio contiene già le basi per capire come funziona il tutto, ovvero: la creazione, la sincronizzazione e la distruzione dei thread. Ovviamente in questa prima parte cominceremo usando la versione base (quasi) universale, ovvero useremo i POSIX Threads. E ora bando alle ciance, vai col codice!

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

// creo un nuovo tipo per passare dei dati ai thread
typedef struct _tdata {
    int             index;      // thread index
    int             *comdata;   // dato comune ai thread
    pthread_mutex_t *lock;      // mutex comune ai thread
} tdata;

// prototipi locali
void* tMyThread(void *arg);

// funzione 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: non posso creare il 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 e crea il 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: non posso creare il thread %d (%s)\n", argv[0], i, strerror(error));
    }

    // join thread e cancella mutex
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);

    // exit
    printf("%s: thread terminati: comdata=%d\n", argv[0], comdata);
    return 0;
}

// thread routine
void* tMyThread(void *arg)
{
    // ottengo i dati del thread con un cast (tdata*) di (void*) arg
    tdata *data = (tdata *)arg;

    // thread loop
    printf("thread %d partito\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 per eventuale uscita dal loop
        if (++i >= 100) {
            // esce dal loop
            break;
        }

        // thread sleep (10 ms)
        usleep(10000);
    }

    // il thread esce
    printf("thread %d finito\n", data->index);
    return NULL;
}

Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale. Supponendo che già sappiate cosa sono e a cosa servono i thread (se no leggetevi prima qualche guida introduttiva, in rete ce ne sono di ottime) il flusso del codice è evidente: prima bisogna creare un mutex (con pthread_mutex_init()) per sincronizzare i thread che useremo, poi bisogna inizializzare i dati da passare ai thread e creare (con pthread_create()) i due thread del nostro esempio (init dati e creazione li ho messi in un loop di 2, ma si potevano anche scrivere ripetendo due volte i passi, ovviamente). Infine il main() si mette in attesa (con pthread_join()) della terminazione dei thread e, quando sono terminati, distrugge il mutex (con pthread_mutex_destroy()) ed esce.

Come si nota pthread_create() ha quattro parametri, che sono (nell'ordine): un pointer a un thread descriptor che identifica univocamente il thread creato, un pointer a un contenitore di attributi del thread da creare, un function pointer alla funzione che esegue il thread e, infine, un pointer all'unico argomento che si può passare alla funzione suddetta. In particolare, nel nostro esempio (semplice semplice), ho usato gli attributi di default (usando NULL per il secondo parametro), e ho creato (con typedef) un nuovo tipo ad-hoc per passare più parametri alla funzione che esegue il thread, sfruttando il fatto che l'argomento di default è un void* che si può facilmente trasformare (con una operazione di cast) in qualsiasi tipo complesso (nel nostro caso nel nuovo tipo tdata).

In questo esempio i due thread creati eseguono la stessa funzione, che ho chiamato tMyThread() (ma avrebbero anche potuto eseguire due funzioni completamente differenti: in questo caso, ovviamente, avrei dovuto scrivere una tMyThread1() e una tMyThread2()). Il flusso della funzione è molto semplice: prima esegue un cast sull'argomento arg per poter usare i dati del tipo tdata, poi entra in un classico thread-loop infinito con uscita forzata: nel nostro caso esce quando l'indice i arriva a 100, ma in un caso reale si potrebbe, per esempio, forzare l'uscita solo in caso di errore. Notare che il thread-loop usa una sleep di 10 ms (usando usleep()): provate a dimenticarvi di mettere la sleep in un thread-loop veramente infinito e vedrete i salti di gioia che farà la CPU del vostro PC!

Come si nota il tipo tdata contiene un indice tipico del thread (nel nostro caso è 0 o 1) e i pointer ai due dati comuni (locali al main()) che sono comdata e lock. Quindi cosa esegue il thread-loop? Visto che è un esempio semplice, si limita a incrementare il dato comune comdata inizializzato nel main() e lo fa in maniera sincronizzata usando pthread_mutex_lock() e pthread_mutex_unlock() sul mutex comune lock: questo serve per evitare che i due thread accedano contemporaneamente a comdata.

Compilando con GCC su macchina Linux (ovviamente) ed eseguendo, il risultato è:

aldo@ao-linux-nb:~/blogtest$ gcc thread.c -o thread -pthread
aldo@ao-linux-nb:~/blogtest$ ./thread 
thread 0 partito
thread 1 partito
thread 1 finito
thread 0 finito
./thread: thread terminati: comdata=200

Che è quello sperato. Nel prossimo post parleremo di una interfaccia alternativa ai POSIX Threads. E, come sempre, vi raccomando di non trattenere il respiro nell'attesa...

Ciao e al prossimo post!

P.S.
Come ben sapete questo è un blog di programmazione con un anima cinefila, per cui vi segnalo (con grande tristezza) che il mese scorso ci ha lasciati un grande maestro. R.I.P., George.