Thread Runner – come usare i thread in C – pt.1

T

Thread Runner
come usare i thread in C – pt.1

Roy Batty: Io ne ho viste cose che voi umani non potreste immaginarvi. Navi da combattimento in fiamme al largo dei bastioni di Orione... e ho visto i raggi B balenare nel buio vicino alle porte di Tannhäuser. E tutti quei momenti andranno perduti nel tempo come lacrime nella pioggia. È tempo di morire.

I thread sono un po’ come i replicanti del mitico capolavoro Blade Runner del Maestro Ridley Scott: sono copie dei loro creatori, sembrano tutti uguali ma poi divergono e hanno una vita propria… e poi terminano, come tristemente descritto nel mitico ed emozionante monologo del bravissimo Rutger Hauer. E quindi oggi parleremo di thread, che è un argomento che ho già più volte affrontato, e in varie sfumature: i problemi con la sleep(3) in Sleep? No, grazie!, il controllo in Totò, Peppino e il Watchdog, il mal uso in Thread Cancel? No, grazie!, l’uso inflazionato in L’invasione dei Multithread, le alternative in Processi o Thread? e poi in molti altri articoli in cui li ho usati negli esempi senza citarli direttamente. Ma ho sempre affrontato l’argomento come se fosse qualcosa di consolidato, qualcosa su cui era inutile entrare in dettagli elementari. Beh, è giunto il momento di di fare un bell’articolo sulle basi, un articolo introduttivo su come usare i thread in C, un articolo che arriva (forse) un po’ in ritardo, ma meglio tardi che mai!

Thread Runner
…come lacrime nella pioggia…

In questo post (che è il primo di tre) 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 terminazione 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!

#define _GNU_SOURCE // per usare strerror_r(3) GNU-specific version
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <pthread.h>

#define NUMTHREADS  2   // numero di thread da trattare, in questo caso 2

// struttura per i dati condivisi dai thread
typedef struct {
    pthread_mutex_t mutex;  // mutex di sincronizzazione comune ai thread
    bool            stop;   // flag per stop thread
    unsigned long   cnt;    // counter condiviso dai thread
} Thdata;

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

// funzione main()
int main(int argc, char* argv[])
{
    char errmsg_buf[256];   // buffer per strerror_r(3)

    // init dei dati condivisi dai thread
    Thdata thdata;
    thdata.stop = false;
    thdata.cnt  = 0;

    // init del mutex di Thdata
    int error;
    if ((error = pthread_mutex_init(&thdata.mutex, NULL)) != 0) {
        // errore!
        printf("%s: non posso creare il mutex (%s)\n",
               argv[0], strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));
        return 1;
    }

    // loop di avvio dei thread
    pthread_t tid[2];
    for (int i = 0; i < NUMTHREADS; i++) {
        // creo un thread
        if ((error = pthread_create(&tid[i], NULL, &threadFunc, (void *)&thdata)) != 0) {
            // errore!
            printf("%s: non posso creare il thread %d (%s)\n",
                   argv[0], i, strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));
            return 1;
        }
    }

    // dopo 10 secondi fermo tutti i thread
    sleep(10);
    thdata.stop = true;

    // loop di join dei thread
    for (int i = 0; i < NUMTHREADS; i++)
        if ((error = pthread_join(tid[i], NULL)) != 0) {
            // errore!
            printf("%s: non posso attendere il thread %d (%s)\n",
                   argv[0], i, strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));
            return 1;
    }

    // cancello il mutex di sincronizzazione
    if ((error = pthread_mutex_destroy(&thdata.mutex)) != 0) {
        // errore!
        printf("%s: non posso cancellare il mutex (%s)\n",
               argv[0], strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));
        return 1;
    }

    // esco
    printf("%s: thread terminati: thdata.cnt=%lu\n", argv[0], thdata.cnt);
    return 0;
}

// threadFunc() - funzione per i thread
void *threadFunc(void *arg)
{
    // ottengo i dati del thread con un cast (Thdata *) di (void *) arg
    Thdata *thdata = (Thdata *)arg;

    // loop del thread
    printf("thread %ld partito\n", pthread_self());
    unsigned long loc_cnt = 0;
    for (;;) {
        // lock del mutex
        pthread_mutex_lock(&thdata->mutex);

        // incremento il counter locale e quello condiviso
        loc_cnt++;
        thdata->cnt++;

        // unlock del mutex
        pthread_mutex_unlock(&thdata->mutex);

        // test dello stop flag
        if (thdata->stop) {
            // il thread esce
            printf("thread %ld terminato dal main: loc_cnt=%lu\n", pthread_self(), loc_cnt);
            pthread_exit(NULL);
        }

        // sleep del thread (uso usleep solo per comodità invece della nanosleep(2))
        usleep(1000);
    }

    // il thread esce per altro motivo diverso dallo stop flag
    printf("thread %ld terminato localmente: loc_cnt=%lu\n", pthread_self(), loc_cnt);
    pthread_exit(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: si inizializzano i dati da passare ai thread, compreso il mutex di sincronizzazione che deve essere creato con pthread_mutex_init(3); dopodiché con pthread_create(3) si creano/avviano i due thread che useremo, usando un loop (ma si potevano anche creare ripetendo due volte i passi, ovviamente). A questo punto si blocca il main() con una sleep(3) di 10 secondi e alla scadenza si attiva il flag di stop dei thread e ci si mette in attesa (usando pthread_join(3)) della terminazione dei thread; quando sono terminati entrambi il main() distrugge il mutex (con pthread_mutex_destroy(3)) ed esce.

Come si nota pthread_create(3) 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 Thdata).

In questo esempio i due thread creati eseguono la stessa funzione, che ho chiamato (con molta originalità, ah aha ah)  threadFunc() (ma avrebbero anche potuto eseguire due funzioni completamente differenti: in questo caso, ovviamente, avrei dovuto scrivere una threadFunc1() e una threadFunc2()). Il flusso della funzione è molto semplice: prima esegue un cast sull’argomento arg per poter usare i dati del tipo Thdata, poi entra in un classico thread-loop infinito con uscita controllata dallo stato dello stop flag inserito nella struttura Thdata. Notare che il thread-loop usa una sleep di 1 ms: 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!

Notare anche che ho usato, per comodità, usleeep(3) ma, come già fatto notare in un altro articolo, bisognerebbe usare sempre nanosleep(2) (usleep(3), come dice il manuale, è deprecata! ). E, comunque, anche la sleep del thread andrebbe usata con parsimonia come ho già scritto in un altro articolo, ci sono maniere più efficienti per controllare un loop infinito…

Ma cosa esegue il thread-loop? In questo semplice esempio si limita a incrementare un contatore locale e il contatore condiviso inizializzato nel main(), e lo fa  in maniera sincronizzata usando pthread_mutex_lock(3) e pthread_mutex_unlock(3) sul mutex condiviso: questo serve per sincronizzare gli accessi ai dati condivisi, evitando che i due thread tentino di modificare contemporaneamente il contatore.

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

aldo@Linux $ gcc -Wall threads.c -o threads -pthread
aldo@Linux $ ./threads
thread 137560485529280 partito
thread 137560475043520 partito
thread 137560475043520 terminato dal main: loc_cnt=8671
thread 137560485529280 terminato dal main: loc_cnt=8670
./threads: thread terminati: thdata.cnt=17341

Notare il numerone corrispondente al thread_id: stiamo stampando il risultato di pthread_self(3)  che è di tipo pthread_t, ossia un tipo opaco (in questo caso anche machine-dependent): su Linux è, normalmente, un long unsigned int, quindi il risultato è corretto usando una printf(3) con format=%lu, ma in altri casi bisognerebbe trattarlo differentemente; comunque per questo semplice esempio va bene così, tanto l’ho stampato solo a titolo informativo.

E i risultati? Sono Ok: il valore dei due contatori è, esattamente quello sperato: ogni thread ha incrementato quando era il suo turno e i due valori locali sono (praticamente) identici e anche il contatore condiviso corrisponde alla somma dei due… perfetto! Thread sincronizzati alla grande!

E per oggi può bastare, non voglio far addormentate nessuno… Nel prossimo post parleremo di una interfaccia alternativa ai POSIX Threads. E, come sempre, vi raccomando di non trattenere il respiro nell’attesa, può nuocere gravemente alla salute!

Ciao e al prossimo post!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti