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.

sabato 16 settembre 2017

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

Dove eravamo rimasti? Ah, si: nella prima parte di Dead Ringers (oops... Thread Ringers) avevamo introdotto l'argomento thread parlando della base, e cioè i POSIX Threads. Ora, come promesso, tenteremo di scrivere lo stesso esempio dello scorso post usando una interfaccia alternativa, e cioè i C11 Threads.
...ti spiego: io sono un POSIX thread e tu un C11 thread...
Una premessa che parte dal lato oscuro della forza (va beh, il C++): il committee ISO del C++ decise di introdurre, nella versione C++11, i thread all'interno del linguaggio. Quindi niente più uso diretto dei POSIX Threads attraverso (ad esempio) la libreria libpthread, ma uso diretto di costrutti del linguaggio. La realizzazione finale è stata (secondo me) brillante, ed i C++11 Threads sono una delle poche cose del C++11 che uso frequentemente (e già sapete cosa ne penso della brutta deriva del C++ pilotata dal committee ISO, se no andate a rileggervi quel post). Il committee ISO del C non poteva rimanere indietro, e quindi hanno pensato di fare la stessa cosa con il C11, quindi i thread adesso fanno direttamente parte del C... o no? Vi anticipo una considerazione: ho una stima del committee ISO del C maggiore di quella che ho del committee ISO del C++ (e non ci voleva molto...), ma in questo caso devo proprio dire che non ci siamo: a seguire vedremo perché.

Come sono stati pensati i nuovi C11 Threads? Allora, hanno preso tutte le funzioni e variabili che compongono i POSIX Threads e gli hanno cambiato il nome (e devo ammettere che quelli nuovi sono più semplici); inoltre, in alcuni casi (pochi, per fortuna), hanno cambiato i tipi dei codici di ritorno e degli argomenti delle funzioni. Punto. Geniale? Non proprio direi, e niente a che vedere con la soluzione brillante usata nel C++11. Motivi per usare questa nuova versione? Zero, direi, e non vi ho ancora esposto il problema principale...

Comunque, ho riscritto l'esempio dello scorso post usando i C11 Threads. Vai col codice!

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

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

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

// funzione main()
int main(int argc, char* argv[])
{
    int error;

    // init mutex
    mtx_t lock;
    if ((error = mtx_init(&lock, mtx_plain)) != thrd_success) {
        printf("%s: non posso creare il mutex (error=%d)\n", argv[0],  error);
        return 1;
    }

    // init threads
    thrd_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 = thrd_create(&tid[i], tMyThread, &data[i])) != thrd_success)
            printf("%s: non posso creare il thread %d (error=%d)\n", argv[0], i, error);
    }

    // join threads e cancella mutex
    thrd_join(tid[0], NULL);
    thrd_join(tid[1], NULL);
    mtx_destroy(&lock);

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

// thread routine
int 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
        mtx_lock(data->lock);

        // incrementa comdata
        (*data->comdata)++;

        // unlock mutex
        mtx_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 0;
}

Come vedete il codice è praticamente identico, mi sono limitato a usare le nuove funzioni al posto di quelle vecchie (per esempio thrd_create() invece di pthread_create()), ho usato i nuovi tipi (per esempio mtx_t invece di pthread_mutex_t) e ho leggermente modificato il test dei valori di ritorno: poche differenze, devo dire, e, in alcuni casi, in peggio: ad esempio è sparito il parametro attr di pthread_create(), che (per semplicità) nello scorso esempio avevo lasciato a NULL, ma che a volte può risultare utile (leggere il manuale di pthread_create() per rendersene conto). Comunque si potrebbe dire (senza fare troppo gli schizzinosi) che la nuova interfaccia non ci offre nessun vantaggio sostanziale, ma neanche un peggioramento decisivo, quindi si potrebbe anche usare (de gustibus).

Ma cè un problema: pare che i C11 Threads non siano considerati una priorità per chi scrive i compilatori e le varie libc, quindi attualmente è difficile compilare/eseguire un programma come quello che ho mostrato. Perfino il nostro amato GCC (che di solito è il primo a fornire supporto per le ultime novità) non supporta i nuovi thread (in realtà a causa della mancata integrazione nella glibc). Quindi, se proprio volete usarli a tutti i costi, dovrete aspettare che qualche compilatore/libreria fornisca il supporto completo, oppure, ad esempio, usare la libreria c11threads che non è altro che un wrapper che simula i C11 Threads usando i POSIX Threads.

Io, alla fine, ho compilato l'esempio usando quella che (credo) sia la soluzione più interessante attualmente disponibile: ho installato nel mio sistema la musl libc che è una libc alternativa alla glibc, ed è dotata di un wrapper per GCC (musl-gcc): musl fornisce (su Linux) il supporto completo al C11, thread inclusi. Una volta compilato il programma si comporta correttamente, come potete vedere qui sotto:

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

Ma il gioco vale la candela? No, per quel che mi riguarda continuerò ad usare i POSIX Threads, che uso da anni e rimangono il riferimento d'eccellenza. Ed un ultimo appunto: a prescindere da quello che stiamo usando (C11/C++11 threads) è molto probabile che, sotto sotto, ci siano i POSIX Threads (è vero in molte implementazioni). E se quando compilate dovete aggiungere il flag -pthread allora il dubbio diventa una certezza, visto che con questo flag usate libpthread ovvero la libreria dei POSIX Threads. Meditate gente, meditate...

Ciao e al prossimo post!

Nessun commento:

Posta un commento