soldato Hudson: Ehi Vasquez, ti hanno mai scambiato per un uomo?
soldato Vasquez: No, e a te?
...è inutile che mi guardi con quella faccia da sleep, soldato Hudson... |
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdbool.h> #include <pthread.h> // creo un nuovo tipo per passare dei dati ai threads typedef struct _tdata { pthread_mutex_t mutex; // mutex comune ai threads bool stop; // flag per stop thread unsigned long comdata; // dato comune ai threads } tdata; // prototipi locali void* faiCose1(void *arg); void* faiCose2(void *arg); // funzione main() int main(int argc, char* argv[]) { pthread_t tid[2]; tdata data; // init mutex int error; if ((error = pthread_mutex_init(&data.mutex, NULL)) != 0) { printf("%s: non posso creare il mutex (%s)\n", argv[0], strerror(error)); return 1; } // init altri dati comuni ai threads data.stop = false; data.comdata = 0; // crea il thread 1 if ((error = pthread_create(&tid[0], NULL, &faiCose1, (void *)&data)) != 0) printf("%s: non posso creare il thread 0 (%s)\n", argv[0], strerror(error)); // crea il thread 2 if ((error = pthread_create(&tid[1], NULL, &faiCose2, (void *)&data)) != 0) printf("%s: non posso creare il thread 1 (%s)\n", argv[0], strerror(error)); // dopo 10 secondi ferma i thread sleep(10); data.stop = true; // join threads e cancella mutex pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&data.mutex); // exit printf("%s: thread terminati: comdata=%ld\n", argv[0], data.comdata); return 0; } // faiCose1() - thread routine void *faiCose1(void *arg) { // ottengo i dati del thread con un cast (tdata *) di (void *) arg tdata *data = (tdata *)arg; // thread loop printf("thread %ld partito\n", pthread_self()); unsigned long i = 0; for (;;) { // lock mutex pthread_mutex_lock(&data->mutex); // fa cose... data->comdata++; i++; // unlock mutex pthread_mutex_unlock(&data->mutex); // test stop flag if (data->stop) { // il thread esce printf("thread %ld terminato dal main (i=%ld comdata=%ld))\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } // thread sleep (uso usleep solo per comodità) usleep(1000); } // il thread esce per altro motivo che lo stop flag printf("thread %ld terminato localmente (i=%ld comdata = %ld)\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } // faiCose2() - thread routine void *faiCose2(void *arg) { // ottengo i dati del thread con un cast (tdata *) di (void *) arg tdata *data = (tdata *)arg; // thread loop printf("thread %ld partito\n", pthread_self()); unsigned long i = 1; for (;;) { // lock mutex pthread_mutex_lock(&data->mutex); // fa cose... data->comdata++; i++; // unlock mutex pthread_mutex_unlock(&data->mutex); // test stop flag if (data->stop) { // il thread esce printf("thread %ld terminato dal main (i=%ld comdata=%ld))\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } // thread sleep (uso usleep solo per comodità) usleep(1000); } // il thread esce per altro motivo che lo stop flag printf("thread %ld terminato localmente (i=%ld comdata = %ld)\n", pthread_self(), i, data->comdata); pthread_exit(NULL); }Tutto chiaro, no? I due thread condividono i dati attraverso la struttura tdata passata come argomento alla loro creazione, e la stessa struttura si usa anche per condividere i mutex di sincronizzazione. Faccio notare (per i più pignoli) che la terza sleep, quella del main() è del tutto giustificata per semplici programmi di esempio come questo, e serve solo a introdurre un ritardo per eseguire lo stop dei thread. Del resto, come già spiegato nella prima parte dell'articolo, questo uso è Ok per per gestire ritardi in single-thread, e il nostro main() può essere ricondotto a questo caso.
E come possiamo modificare questa applicazione per eseguire la sincronizzazione in modo efficace e intelligente come anticipato all'inizio? Ma usando, ad esempio, una condition variable ("Elementare, mio caro Watson!"). E allora riscriviamo il codice nella seguente maniera:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdbool.h> #include <pthread.h> // creo un nuovo tipo per passare dei dati ai threads typedef struct _tdata { pthread_mutex_t mutex; // mutex comune ai threads pthread_cond_t cond; // condition variable comune ai thread bool stop; // flag per stop thread bool ready; // flag per dati disponibili unsigned long comdata; // dato comune ai threads } tdata; // prototipi locali void* faiCose1(void *arg); void* faiCose2(void *arg); // funzione main() int main(int argc, char* argv[]) { pthread_t tid[2]; tdata data; // init mutex int error; if ((error = pthread_mutex_init(&data.mutex, NULL)) != 0) { printf("%s: non posso creare il mutex (%s)\n", argv[0], strerror(error)); return 1; } // init condition variable if ((error = pthread_cond_init(&data.cond, NULL)) != 0) { printf("%s: non posso creare il cond (%s)\n", argv[0], strerror(error)); return 1; } // init altri dati comuni ai threads data.stop = false; data.ready = false; data.comdata = 0; // crea il thread 1 if ((error = pthread_create(&tid[0], NULL, &faiCose1, (void *)&data)) != 0) printf("%s: non posso creare il thread 0 (%s)\n", argv[0], strerror(error)); // crea il thread 2 if ((error = pthread_create(&tid[1], NULL, &faiCose2, (void *)&data)) != 0) printf("%s: non posso creare il thread 1 (%s)\n", argv[0], strerror(error)); // dopo 10 secondi ferma i thread sleep(10); data.stop = true; // join threads e cancella mutex pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&data.mutex); pthread_cond_destroy(&data.cond); // exit printf("%s: thread terminati: comdata=%ld\n", argv[0], data.comdata); return 0; } // faiCose1() - thread routine void *faiCose1(void *arg) { // ottengo i dati del thread con un cast (tdata *) di (void *) arg tdata *data = (tdata *)arg; // thread loop printf("thread %ld partito\n", pthread_self()); unsigned long i = 0; for (;;) { // lock mutex pthread_mutex_lock(&data->mutex); // aspetta condizione while (!data->ready) pthread_cond_wait(&data->cond, &data->mutex); // fa cose... data->comdata++; i++; // segnala condizione data->ready = false; pthread_cond_signal(&data->cond); // unlock mutex pthread_mutex_unlock(&data->mutex); // test stop flag if (data->stop) { // il thread esce printf("thread %ld terminato dal main (i=%ld comdata=%ld))\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } } // il thread esce per altro motivo che lo stop flag printf("thread %ld terminato localmente (i=%ld comdata = %ld)\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } // faiCose2() - thread routine void *faiCose2(void *arg) { // ottengo i dati del thread con un cast (tdata *) di (void *) arg tdata *data = (tdata *)arg; // thread loop printf("thread %ld partito\n", pthread_self()); unsigned long i = 1; for (;;) { // lock mutex pthread_mutex_lock(&data->mutex); // aspetta condizione while (data->ready) pthread_cond_wait(&data->cond, &data->mutex); // fa cose... data->comdata++; i++; // segnala condizione data->ready = true; pthread_cond_signal(&data->cond); // unlock mutex pthread_mutex_unlock(&data->mutex); // test stop flag if (data->stop) { // il thread esce printf("thread %ld terminato dal main (i=%ld comdata=%ld))\n", pthread_self(), i, data->comdata); pthread_exit(NULL); } } // il thread esce per altro motivo che lo stop flag printf("thread %ld terminato localmente (i=%ld comdata = %ld)\n", pthread_self(), i, data->comdata); pthread_exit(NULL); }Che vi sembra? Con pochi cambi mirati, che consistono solo nell'aggiungere una condition variable e un flag di dati disponibili nei dati comuni (nella struttura tdata), e sorvegliando variabile e flag nei thread, si può evitare di usare le sleep (al prezzo di un leggero aumento dell'uso di CPU, molto lontano dal 100% di occupazione) e ottenendo una sincronizzazione precisa e deterministica (i thread lavorano solo quando serve): infatti usando le sleep la sincronizzazione è abbastanza alla spera in Dio, e aggiunge all'esecuzione dell'attività che ci interessa un ritardo innecessario e arbitrario (solitamente calcolato in maniera empirica o, peggio, messo un po' a caso) e sempre sperando che il thread-scheduler faccia un buon lavoro.
Risulta anche evidente che l'uso di una condition vartiable è abbastanza semplice e immediato. Tutti i segreti sono contenuti nelle ottime pagine del POSIX Programmer's Manual ma, comunque, vorrei evidenziare alcuni punti:
- Una condition variable è sempre associata a un mutex (si nota anche dal prototipo della pthread_cond_wait()) con cui lavora in completa sinergia.
- Il test di attesa sulla variazione deve essere messo in un while loop, cioè deve essere un test continuo e ripetitivo.
- Bisogna sempre aggiungere al meccanismo una variabile normale di sincronizzazione (è la variabile ready dell'esempio), per gestire il loop di attesa.
- Non aspettatevi miracoli: anche le condition variable hanno i loro punti critici e non sono la soluzione definitiva, sono solo una soluzione. Ma offrono già un bel miglioramento rispetto al semplice uso della sleep.
Come si intuisce dal punto 4 qua sopra, ci sono anche altri metodi-di-sincronizzazione-senza-sleep: io ho scelto quello con la condition variable perchè mi sembra che renda bene l'idea di come ragionare scrivendo applicazioni multithread (e ribadisco che è anche semplice da usare). Comunque, in generale, i metodi di sincronizzazione prevedono l'uso di spin lock, semafori, mutex ed eventi vari, spesso usati in combinazione. Ma ora non mi sembra il caso di aggiungere altra carne al fuoco: l'esempio con la variable condition è più che sufficiente per farsi una idea su come operare.
Ok, direi che il compito che mi ero prefisso, la demitizzazione della sleep, è completato. Con il prossimo articolo cambieremo argomento (o forse no? Magari sarebbe il momento di battere il ferro finché è caldo e proporre subito qualche altro esempio: vedremo). Comunque, non dormite troppo nel frattempo, la sleep non funziona, e chi dorme non piglia pesci!
Ciao, e al prossimo post!
Nessun commento:
Posta un commento