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!
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!