Scrivere Software è un piacere. Un programma non solo deve funzionare bene ed essere efficiente (questo si dà per scontato), ma deve essere anche bello ed elegante da leggere, comprensibile e facile da manutenere, sia per l'autore che per eventuali lettori futuri. Programmare bene in C è un'arte.
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 23 dicembre 2017
sabato 9 dicembre 2017
Remapped File
come sincronizzare un Memory Mapped File in C - pt.1
Questo post è un remake. È il remake di un mio post (in due puntate) di quasi due anni fa, Irrational File (forza, forza, andate a rileggerlo, non fatevi pregare!). Normalmente i remake lasciano l'amaro in bocca, non aggiungono nulla di nuovo e, se l'originale era un gran film, difficilmente riescono a eguagliarlo. Ci sono delle eccezioni, però, come The Thing di J.Carpenter (uff, anche questo film l'ho già usato...) che è un capolavoro superiore al (pur buono) originale del 1951. Ecco, questo post appartiene (spero) ai remake buoni, perché, come vedrete, aggiunge molto all'originale.
Allora, il post originale descriveva una (semplice) libreria che avevo scritto per condividere dati tra processi (IPC) usando come mezzo di comunicazione un Memory Mapped File. In una frase del vecchio post (che riporto parzialmente) si preannunciava già questo remake: "...per un uso semplice questo metodo va più che bene, ma, per applicazioni più complesse [...] bisognerebbe usare un sistema più evoluto [...] un mutex o un semaforo (ma questa è un altra storia, magari in futuro farò un post sull'argomento)...". Ecco, ora ci siamo: questa è la versione con gli accessi sincronizzati della nostra vecchia libmmap, quasi pronta per un uso professionale.
Ripetendo la struttura del vecchio post (e se no, che remake sarebbe?) ho diviso il tutto in due puntate: nella prima descriverò, a mo' di specifica funzionale, l'header file (libmmap.h) e un esempio di uso (data.h, datareader.c e datawriter.c) basato su due applicazioni comunicanti. Nella seconda puntata descriverò la vera e propria implementazione della libreria (libmmap.c).
Cominciamo: vediamo l'header-file, libmmap.h:
Il flag di data_ready si usa (protetto dal semaforo) per segnalare la disponibilità di nuovi dati da leggere. Poi abbiamo, dulcis in fundo, i campi len e data che ci conducono al secondo obiettivo che mi ero prefisso: i dati scambiati sono, ora, generici, con formato e lunghezza che vengono decisi a livello applicativo. Notare, per l'appunto, che il campo data è un array di dimensione 1: questo è una sorta di trucco (da usare con le dovute precauzioni) abbastanza usato nel C per trattare dati generici di forma e lunghezza non disponibili a priori. Nella nostra struct il campo deve essere, ovviamente, posto come ultimo membro, rendendola così una specie di struttura con size variabile. Comunque nel prossimo post vedremo meglio come funziona il tutto.
libmmap.h termina con i prototipi delle funzioni che compongono la libreria: abbiamo due funzioni di apertura, che ci permettono di aprire un mapped-file in modo Master o Slave (nel prossimo post vedremo il perché di questa doppia apertura); poi abbiamo una funzione di chiusura, una di flush, e, ovviamente, due funzioni per scrivere e leggere dati. Queste ultime due funzioni ci confermano il discorso di genericità appena descritto: le variabili di read/write sono di tipo void*, quindi adatte ad accettare qualsiasi tipo di dato. Visto che il formato dei dati da scambiare viene spostato a livello applicativo ho scritto un header-file (di esempio), data.h, che viene incluso dalle due applicazioni comunicanti:
Usando le due applicazioni in due terminali differenti cosa vedremo?
Per oggi abbiamo finito. In attesa della seconda parte potreste provare a immaginare a come sarà l'implementazione della libreria che vi presenterò... ma si avvicina il Natale e immagino (e spero) che abbiate cose più interessanti da pensare in questo periodo...
Ciao, e al prossimo post!
![]() |
...un immagine di The Thing per il remake di Irrational Man: poche idee e ben confuse, vedo... |
Ripetendo la struttura del vecchio post (e se no, che remake sarebbe?) ho diviso il tutto in due puntate: nella prima descriverò, a mo' di specifica funzionale, l'header file (libmmap.h) e un esempio di uso (data.h, datareader.c e datawriter.c) basato su due applicazioni comunicanti. Nella seconda puntata descriverò la vera e propria implementazione della libreria (libmmap.c).
Cominciamo: vediamo l'header-file, libmmap.h:
#ifndef LIBMMAP_H #define LIBMMAP_H #include <semaphore.h> #include <stdbool.h> #define MAPNAME "/shmdata" // struttura del mapped-file typedef struct { sem_t sem; // semaforo di sincronizzazione accessi bool data_ready; // flag di data ready (true=ready) size_t len; // lunghezza campo data char data[1]; // dati da condividere } ShmData; // prototipi globali ShmData *memMapOpenMast(const char *shmname, size_t len); ShmData *memMapOpenSlav(const char *shmname, size_t len); int memMapClose(const char *shmname, ShmData *shmdata); int memMapFlush(ShmData *shmdata); int memMapRead(void *dest, ShmData *src); void memMapWrite(ShmData *dest, const void *src); #endif /* LIBMMAP_H */Semplice e auto-esplicativo, no? La nostra libreria usa una struttura dati ShmData per mappare il mapped-file: qui subito notiamo il primo grosso miglioramento ottenuto: c'è un semaforo (un POSIX unnamed semaphore) per sincronizzare gli accessi alla memoria, il che rende la nostra libreria adatta a un vero uso multitask (e multithread), che era il primo obiettivo programmato. Il meccanismo di sincronizzazione l'ho scelto in maniera oculata tra i vari disponibili: magari un giorno scriverò un post specifico sull'argomento, ma, per il momento, mi limiterò a dire che il metodo scelto è semplice da implementare, molto funzionale e si adatta perfettamente allo scopo da raggiungere (e scusate se è poco!).
Il flag di data_ready si usa (protetto dal semaforo) per segnalare la disponibilità di nuovi dati da leggere. Poi abbiamo, dulcis in fundo, i campi len e data che ci conducono al secondo obiettivo che mi ero prefisso: i dati scambiati sono, ora, generici, con formato e lunghezza che vengono decisi a livello applicativo. Notare, per l'appunto, che il campo data è un array di dimensione 1: questo è una sorta di trucco (da usare con le dovute precauzioni) abbastanza usato nel C per trattare dati generici di forma e lunghezza non disponibili a priori. Nella nostra struct il campo deve essere, ovviamente, posto come ultimo membro, rendendola così una specie di struttura con size variabile. Comunque nel prossimo post vedremo meglio come funziona il tutto.
libmmap.h termina con i prototipi delle funzioni che compongono la libreria: abbiamo due funzioni di apertura, che ci permettono di aprire un mapped-file in modo Master o Slave (nel prossimo post vedremo il perché di questa doppia apertura); poi abbiamo una funzione di chiusura, una di flush, e, ovviamente, due funzioni per scrivere e leggere dati. Queste ultime due funzioni ci confermano il discorso di genericità appena descritto: le variabili di read/write sono di tipo void*, quindi adatte ad accettare qualsiasi tipo di dato. Visto che il formato dei dati da scambiare viene spostato a livello applicativo ho scritto un header-file (di esempio), data.h, che viene incluso dalle due applicazioni comunicanti:
#ifndef DATA_H #define DATA_H // definizione struttura data per applicativi di esempio typedef struct { int type; // tipo di dati int data_a; // un dato (esempio) int data_b; // un altro dato (esempio) char text[1024]; // testo dei dati } Data; #endif /* DATA_H */Come vedete ho scelto di usare una struttura dati che include un campo testo, ma si può scambiare qualsiasi cosa, anche solo un semplice int, per esempio. Vediamo ora la prima applicazione d'uso, datawriter.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programma di test int main(int argc, char *argv[]) { // apre mapped-file ShmData *shmdata; if ((shmdata = memMapOpenMast(MAPNAME, sizeof(Data)))) { // file aperto: start loop di scrittura for (int i = 0; i < 100; i++) { // compone dati per il reader Data data; snprintf(data.text, sizeof(data.text), "nuovi dati %d", i); // scrive dati nel mapped-file memMapWrite(shmdata, &data); printf("ho scritto: %s\n", data.text); // loop sleep mySleep(100); } // chiude mapped-file memMapClose(MAPNAME, shmdata); } else { // esce con errore printf("non posso aprire il file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // esce con Ok return EXIT_SUCCESS; }Semplice, no? Apre il file condiviso in memoria (in modo Master) e lo usa per scrivere dati (in loop) per l'altra applicazione di test, datareader.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programma di test int main(int argc, char *argv[]) { // apre aspettando che un writer apra come master il mapped-file ShmData *shmdata; while ((shmdata = memMapOpenSlav(MAPNAME, sizeof(Data))) == NULL) { // accetta solo l'errore di file non ancora esistente if (errno != ENOENT) { // esce con errore printf("non posso aprire il file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // loop sleep mySleep(100); } // file aperto: start loop di lettura for (int i = 0; i < 100; i++) { // cerca dati da leggere nel mapped-file Data data; if (memMapRead(&data, shmdata)) { // mostra i dati letti printf("mi hai scritto: %s\n", data.text); } // loop sleep mySleep(100); } // chiude mapped-file ed esce con Ok memMapClose(MAPNAME, shmdata); return EXIT_SUCCESS; }Il reader è, come si vede, una applicazione speculare del writer (legge invece di scrivere). Notare che, in entrambe le applicazioni, si testano gli errori sulle funzioni di open e si chiude (se necessario) l'esecuzione mostrando l'errore con strerror(): questo è possibile perché (come vedremo nel prossimo post) le funzioni di apertura escono in caso di errore delle funzioni della libc che usano internamente, e, a quel punto, è disponibile con errno la descrizione dell'errore che si è verificato (ma di questo ne abbiamo parlato ampiamente negli ultimi due post, ricordate?).
Usando le due applicazioni in due terminali differenti cosa vedremo?
Nel terminale 1: aldo@mylinux:~/blogtest$ ./datawriter ho scritto: nuovi dati 1 ho scritto: nuovi dati 2 ho scritto: nuovi dati 3 ^C Nel terminale 2: aldo@mylinux:~/blogtest$ ./datareader mi hai scritto: nuovi dati 1 mi hai scritto: nuovi dati 2 mi hai scritto: nuovi dati 3Per mandare a dormire i loop ho usato la funzione mySleep(), che è una nostra vecchia conoscenza: si pùo inserire in una libreria a parte o nella stessa libreria libmmap (io ho usato un file a parte mysleep.c con un suo header-file mysleep.h che contiene solo il prototipo). In questo semplice esempio le due applicazioni comunicanti si possono fermare usando CTRL-C, e (quando proverete a usare la libreria) potrete verificare che avviando/fermando/riavviando entrambe le applicazioni, in qualsiasi ordine, si ri-sincronizzano sempre senza problemi.
Per oggi abbiamo finito. In attesa della seconda parte potreste provare a immaginare a come sarà l'implementazione della libreria che vi presenterò... ma si avvicina il Natale e immagino (e spero) che abbiate cose più interessanti da pensare in questo periodo...
Ciao, e al prossimo post!
sabato 25 novembre 2017
Errno e i suoi fratelli
come è implementato errno in C
Questo post è un doveroso addendum al post precedente (che se non avete già letto dovreste affrettarvi a farlo, sono strettamente collegati). Questa volta parleremo di errno e dei suoi fratelli, una famiglia complessa, come quella di Rocco e i suoi fratelli, un capolavoro del neorealismo Italiano.
Veniamo al dunque: dopo aver letto l'ultimo post i lettori più attenti si saranno chiesti: "Ok, con strerror_r() possiamo trattare le stringhe di errore in modo thread-safe, ma cosa ce ne facciamo se poi, alla base di tutto, c'è la variabile globale errno che non ha proprio l'aria di essere thread-safe?". La domanda è lecita, e per rispondere dobbiamo tornare un po' indietro nel tempo... la storia è analoga e parallela a quella di strerror() (e ci mancherebbe solo!). Anticamente errno era definito così nel header errno.h:
E, per finire in bellezza, non facciamoci mancare un piccolo estratto dello standard POSIX.1c, che puntualizza tutto quello detto fin'ora:
Ciao, e al prossimo post!
![]() |
Rocco Errno e i suoi fratelli |
extern int errno;e si riferiva a una semplice variabile globale della libc, per l'appunto un int chiamato errno. Poi sono arrivati i thread, con lo standard POSIX 1003.1c (detto anche POSIX.1c, ma fa lo stesso), e con lui è arrivata anche la strerror_r() e, come no, anche errno ha avuto un fratello thread-safe. Come si può ben leggere nel manuale di errno (dopo POSIX.1c) ora errno è:
errno is defined by the ISO C standard to be a modifiable lvalue of type int, and must not be explicitly declared; errno may be a macro. errno is thread-local; setting it in one thread does not affect its value in any other thread.Quindi la nuova definizione di errno è ora in bits/errno.h (che viene incluso dal errno.h classico). Semplificando un po' (ho omesso alcuni dettagli per facilitare la lettura) il nuovo giro del fumo è:
nel header-file errno.h #include <bits/errno.h> /* Declare the `errno' variable, unless it's defined as a macro by bits/errno.h. This is the case in GNU, where it is a per-thread variable. This redeclaration using the macro still works, but it will be a function declaration without a prototype and may trigger a -Wstrict-prototypes warning. */ #ifndef errno extern int errno; #endif nel header-file bits/errno.h /* Function to get address of global `errno' variable. */ extern int *__errno_location (void); /* When using threads, errno is a per-thread value. */ #define errno (*__errno_location ())Quindi, in parole povere, ora errno non è più un int globale ma è "il contenuto di un indirizzo ritornato da una funzione globale". Ovviamente la variabile int a cui punta la funzione in oggetto è il nuovo errno locale di un thread (ossia: ogni thread ha il suo errno). Un esempio (mooolto semplificato) di come si potrebbe implementare la __errno_location() è il seguente:
// errno locale di un thread: è una variabile di tipo Thread-local storage (TLS) __thread int th_errno; int *__errno_location(void) { // ritorna l'indirizzo della variabile th_errno return &th_errno; }E, alla fine della fiera, nonostante i cambi descritti, sarà ancora possibile fare operazioni di questo tipo:
int my_errno = errno; // Ok, equivale a: int my_errno = (* __errno_location()); errno = EADDRINUSE; // Ok, equivale a: (* __errno_location()) = EADDRINUSE;perché, ovviamente, tutto è stato pensato per essere retro-compatibile, e quindi errno, nonostante ora sia una macro, deve ancora comportarsi come quando era un semplice int.
E, per finire in bellezza, non facciamoci mancare un piccolo estratto dello standard POSIX.1c, che puntualizza tutto quello detto fin'ora:
Redefinition of errno In POSIX.1, errno is defined as an external global variable. But this definition is unacceptable in a multithreaded environment, because its use can result in nondeterministic results. The problem is that two or more threads can encounter errors, all causing the same errno to be set. Under these circumstances, a thread might end up checking errno after it has already been updated by another thread. To circumvent the resulting nondeterminism, POSIX.1c redefines errno as a service that can access the per-thread error number as follows (ISO/IEC 9945:1-1996, n2.4): Some functions may provide the error number in a variable accessed through the symbol errno. The symbol errno is defined by including the header <errno.h>, as specified by the C Standard ... For each thread of a process, the value of errno shall not be affected by function calls or assignments to errno by other threads. In addition, all POSIX.1c functions avoid using errno and, instead, return the error number directly as the function return value, with a return value of zero indicating that no error was detected. This strategy is, in fact, being followed on a POSIX-wide basis for all new functions.Notare che l'ultima frase (da In addition... in poi) ci spiega il perchè dell'esistenza della strerror_r() XSI-compliant descritta nel post precedente: visto? tutti i nodi vengono al pettine... E con questo possiamo considerare risolto anche il mistero dell'errno thread-safe. Missione compiuta!
Ciao, e al prossimo post!
venerdì 10 novembre 2017
Strerror e le sue sorelle
quale strerror scegliere in C
Questa è una storia di strani incontri e rapporti sbagliati, come nel capolavoro (l'ennesimo) del grande Woody, "Hannah e le sue sorelle". La vita, spesso ci porta a prendere decisioni importanti, come è successo ad Hannah, e altre un po' meno importanti come quelle che analizzeremo tra poco... comunque sempre di decisioni si tratta.
(apro una parentesi: in questo post parleremo della strerror() e delle sue varianti. La strerror() è una funzione della libc che, passandogli un numero di errore, ti restituisce la stringa descrittiva corrispondente. Molte funzioni di libreria e system calls in caso di errore aggiornano il valore di una variabile globale, errno, che quindi contiene, in ogni momento, il valore dell'ultimo errore di esecuzione. Esiste poi un altra variabile globale, _sys_errlist, che contiene le stringhe corrispondenti a ogni errno, per cui, prima che qualche altra parte del programma in esecuzione alteri il valore di errno, bisognerebbe localizzare in _sys_errlist la stringa di errore che vogliamo trattare. Come anticipato, questa ultima operazione si può fare usando la strerror(), di cui abbiamo già parlato qui in maniera indiretta. Chiudo la parentesi)
Si era detto: decisioni. La strerror() ha molte personalità, quindi quale scelgo? la strerror() o la strerror_r()? E se uso quest'ultima quale scelgo, la versione XSI-compliant o la versione GNU-specific? (per non parlare, poi, delle altre varianti, la strerror_l(), la strerror_s(), ecc., ma queste sono varianti secondarie).
Cominciamo con la prima domanda: strerror() o strerror_r()? Anticamente esisteva solo la prima, ma poi sono apparsi i thread e sono cominciati i problemi, perché nel codice multi-thread ci sono alcune parti critiche dove bisognerebbe usare solo funzioni thread-safe. La strerror() non è dichiarata thread-safe nello standard, e per capire il perché basta analizzare una implementazione semplificata (però molto simile alle implementazioni reali che possiamo trovare nelle varie libc disponibili). Vai col codice!
(altra parentesi: non è impossibile scrivere una strerror() che sia thread-safe, e in alcuni sistemi lo è: ma visto che secondo lo standard non lo è, non possiamo essere sicuri che sul sistema che stiamo usando (o sul sistema su cui, un giorno, girerà la applicazione che stiamo scrivendo) non ci sia una implementazione come quella appena descritta, quindi...)
Allora, per il Software multi-thread è nata la strerror_r() che è thread-safe. Come funziona? vai col codice!
Ed ora aggiungiamo un po' di complicazione: la versione di strerror_r() appena mostrata è la GNU-specific. Ma, sfortunatamente, esiste anche la XSI-compliant, che è la seguente:
E ora siamo pronti per la seconda decisione: quale usiamo, la GNU-specific o la XSI-compliant? Beh, io direi che quando scriviamo del codice per trattare dei codici di errore probabilmente non ci interessa trattare anche gli errori generati in questa fase (e nella fase successiva, ecc., ecc., un loop infinito di ricerca degli errori!); ci interessa, invece, scrivere codice lineare e semplice... per toglierci il dubbio possiamo analizzare due piccoli esempi d'uso:
Ciao, e al prossimo post!
![]() |
Hannah Strerror e le sue sorelle |
Si era detto: decisioni. La strerror() ha molte personalità, quindi quale scelgo? la strerror() o la strerror_r()? E se uso quest'ultima quale scelgo, la versione XSI-compliant o la versione GNU-specific? (per non parlare, poi, delle altre varianti, la strerror_l(), la strerror_s(), ecc., ma queste sono varianti secondarie).
Cominciamo con la prima domanda: strerror() o strerror_r()? Anticamente esisteva solo la prima, ma poi sono apparsi i thread e sono cominciati i problemi, perché nel codice multi-thread ci sono alcune parti critiche dove bisognerebbe usare solo funzioni thread-safe. La strerror() non è dichiarata thread-safe nello standard, e per capire il perché basta analizzare una implementazione semplificata (però molto simile alle implementazioni reali che possiamo trovare nelle varie libc disponibili). Vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili // globali _sys_errlist (array errori) e _sys_nerr (num.errori) static char buf[256]; // buffer globale statico per la stringa da ritornare char *strerror(int errnum) { // test se errnum è un valore valido if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) { // errore sconosciuto: copio in buf un messaggio di errore generico snprintf(buf, sizeof(buf), "Unknown error %d", errnum); } else { // errore conosciuto: copio in buf il messaggio corrispondente snprintf(buf, sizeof(buf), "%s", _sys_errlist[errnum]); } // ritorno buf che ora contiene il messaggio di errore return buf; }risulta evidente dal codice (ben commentato, come sempre, così non devo spiegarlo riga per riga) che la strerror() non ritorna direttamente _sys_errlist[errnum] (e se fosse così sarebbe thread-safe) ma compone un messaggio di errore (per trattare anche gli errnum non validi) usando un buffer globale statico buf: quindi, se due thread di una applicazione usano (quasi) contemporaneamente la strerror() il contenuto di buf non sarà attendibile (prevale il thread che ha scritto per ultimo).
(altra parentesi: non è impossibile scrivere una strerror() che sia thread-safe, e in alcuni sistemi lo è: ma visto che secondo lo standard non lo è, non possiamo essere sicuri che sul sistema che stiamo usando (o sul sistema su cui, un giorno, girerà la applicazione che stiamo scrivendo) non ci sia una implementazione come quella appena descritta, quindi...)
Allora, per il Software multi-thread è nata la strerror_r() che è thread-safe. Come funziona? vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili // globali _sys_errlist (array errori) e _sys_nerr (num.errori) char *strerror_r(int errnum, char *buf, size_t buflen); { // test se errnum è un valore valido if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) { // errore sconosciuto: copio in buf un messaggio di errore generico snprintf(buf, buflen, "Unknown error %d", errnum); } else { // errore conosciuto: copio in buf il messaggio corrispondente snprintf(buf, buflen, "%s", _sys_errlist[errnum]); } // ritorno buf che ora contiene il messaggio di errore return buf; }anche in questo caso si tratta di codice semplificato, ma molto vicino alla realtà: il trucco è semplice, invece di usare un buffer globale statico (che è la fonte dei problemi della strerror()) il chiamante della funzione si deve preoccupare di allocare e passare un buffer (e la sua lunghezza) alla strerror_r(). In questo modo il buffer che usa la strerror_r() è locale al thread che la chiama, e non può essere sovrascritto da un altro thread concorrente. Abbiamo sacrificato un po' di semplicità d'uso ma abbiamo ottenuto l'agognato comportamento thread-safe!
Ed ora aggiungiamo un po' di complicazione: la versione di strerror_r() appena mostrata è la GNU-specific. Ma, sfortunatamente, esiste anche la XSI-compliant, che è la seguente:
int strerror_r(int errnum, char *buf, size_t buflen);Come si nota questa seconda versione non ritorna il buffer con la error-string, ma ritorna, invece, un codice di errore, e la stringa trovata bisogna ripescarla direttamente nel buffer che abbiamo passato. Per quanto riguarda il codice di ritorno è 0 in caso di successo e, in base alla versione di libc in uso, potrebbe ritornare -1 in caso di errore (settando errno al valore specifico di errore) oppure un valore positivo corrispondente a errno (bah, questo doppio comportamento non è proprio il massimo della semplicità d'uso...). Per usare questa versione o la GNU-specific bisogna giocare opportunamente con i flag _GNU_SOURCE, _POSIX_C_SOURCE e _XOPEN_SOURCE del preprocessore (come descritto nel manuale della strerror()).
E ora siamo pronti per la seconda decisione: quale usiamo, la GNU-specific o la XSI-compliant? Beh, io direi che quando scriviamo del codice per trattare dei codici di errore probabilmente non ci interessa trattare anche gli errori generati in questa fase (e nella fase successiva, ecc., ecc., un loop infinito di ricerca degli errori!); ci interessa, invece, scrivere codice lineare e semplice... per toglierci il dubbio possiamo analizzare due piccoli esempi d'uso:
GNU-specific if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // errore socket() char errbuf[MAX_ERROR_LEN]; // buffer per strerror_r() printf("socket() error (%s)\n", strerror_r(errno, errbuf, sizeof(errbuf))); return EXIT_FAILURE; }
XSI-compliant if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // errore socket() char errbuf[MAX_ERROR_LEN]; // buffer per strerror_r() int my_error = strerror_r(errno, errbuf, sizeof(errbuf))); if (! my_error) printf("socket() error (%s)\n", errbuf); else { // tratto l'errore (magari usando di nuovo strerror_r()?) ... } return EXIT_FAILURE; }Non so voi cosa ne pensate, ma io uso sempre la versione GNU-specific! A voi la scelta...
Ciao, e al prossimo post!
sabato 7 ottobre 2017
Thread Ringers
come usare i thread in C - pt.3
Con questo post chiudiamo (in bellezza, spero) il mini-ciclo sui thread (Dead Ringers per gli amici).
Dopo gli esempi base delle prime due parti del ciclo (che avete appena riletto, vero? qui e qui), è il caso di fare un esempio reale di una delle tante applicazioni che possono avere i thread. E tra le tante ne ho scelto una che mi sembra interessante, ovvero un Socket Server multithread, dove ogni connessione con un Client remoto viene gestita con un thread separato. Una raccomandazione: prima di andare avanti dovreste rileggere un mio vecchio post, e cioè: Il Server oscuro - Il ritorno, che è una ideale introduzione all'argomento in corso, visto che descrive (e bene, spero) funzionalità e codice di un Socket Server single-thread. Tra l'altro (come noterete tra poco) il nuovo codice che vi mostrerò è parente strettissimo di quello mostrato nel vecchio post.
E ora bando alle ciance, vai col codice!
Ok, non stiamo a raccontare di nuovo come funziona un Socket Server (già fatto nel vecchio post, rileggere attentamente, please), ma concentriamoci sulle differenze tra il codice single-thread e quello multithread: sicuramente avrete notato che sono praticamente identici fino alla fase di listen(), e anche dopo le differenze sono minime: la fase di accept() adesso è in un loop, e per ogni connessione accettata (di un Client remoto) viene creato un nuovo thread. E cosa esegue il thread? Esegue la funzione connHandler() che contiene, guarda caso, il loop di recv() che nel vecchio codice era eseguito subito dopo la fase di accept(). Anche il successivo test del motivo di uscita (prematura) dal loop è contenuto in connHandler(), e mostra il corretto segnale di errore (recv() error o client disconnected, in base al codice ritornato dalla recv()).
Cosa aggiungere? Semplice e super-funzionale: un Socket Server multithread con quattro righe di codice! Ovviamente la sintassi di creazione del thread e l'esecuzione della start_routine dello stesso sono identiche a quelle descritte qui. Per testare il nostro Socket Server è necessario compilare anche un Socket Client (ovviamente quello descritto nel mio vecchio post Il Client oscuro - Il ritorno), ed eseguire, ad esempio, una istanza del Socket Server e due istanze del Socket Client (in tre terminali diversi della stessa macchina, oppure su tre macchine diverse). Eseguendo sulla mia macchina (Linux, ovviamente) su tre terminali il risultato è il seguente:
Nel terminale 1:
Nel terminale 2:
Nel terminale 3:
notare che quando uno dei Client esce (con un CTRL-C, ad esempio) il Server se ne accorge e visualizza, come previsto, client disconnected... perfetto.
Ok, con i thread abbiamo finito. Adesso cercherò di pensare a qualche nuovo interessante argomento per il prossimo post. Come sempre vi invito a non trattenere il respiro nell'attesa...
Ciao e al prossimo post!
![]() |
...proprio l'immagine che uno si aspetta in un blog di programmazione... |
E ora bando alle ciance, vai col codice!
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <errno.h> #include <pthread.h> #define BACKLOG 10 // per listen() #define MYBUFSIZE 1024 // prototipi locali void *connHandler(void *conn_sock); int main(int argc, char *argv[]) { // test argomenti if (argc != 2) { // errore args printf("%s: numero argomenti errato\n", argv[0]); printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]); return EXIT_FAILURE; } // crea un socket int my_socket; if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // errore socket() printf("%s: could not create socket (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // prepara la struttura sockaddr_in per questo server struct sockaddr_in server; // (local) server socket info server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(atoi(argv[1])); // bind informazioni del server al socket if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) { // errore bind() printf("%s: bind failed (%s)", argv[0], strerror(errno)); return EXIT_FAILURE; } // start ascolto con una coda di max BACKLOG connessioni if (listen(my_socket, BACKLOG) == -1) { // errore listen() printf("%s: listen failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // accetta connessioni da un client entrante printf("%s: attesa connessioni entranti...\n", argv[0]); pthread_t thread_id; socklen_t socksize = sizeof(struct sockaddr_in); struct sockaddr_in client; // (remote) client socket info int client_sock; while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) { printf("%s: connessione accettata\n", argv[0]); if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) { // errore pthread_create() printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } } // errore accept() printf("%s: accept failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // thread function per gestione connessioni void *connHandler(void *conn_sock) { // estrae il client socket dall'argomento int client_sock = *(int*)conn_sock; // loop di ricezione messaggi dal client int read_size; char client_msg[MYBUFSIZE]; while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) { // send messaggio di ritorno al client printf("%s: ricevuto messaggio dal sock %d: %s\n", __func__, client_sock, client_msg); char server_msg[MYBUFSIZE]; sprintf(server_msg, "mi hai scritto: %s", client_msg); send(client_sock, server_msg, strlen(server_msg), 0); // clear buffer memset(client_msg, 0, MYBUFSIZE); } // loop terminato: test motivo if (read_size == -1) { // errore recv() printf("%s: recv failed\n", __func__); } else { // read_size == 0: il client si è disconnesso printf("%s: client disconnected\n", __func__); } return NULL; }
Ok, non stiamo a raccontare di nuovo come funziona un Socket Server (già fatto nel vecchio post, rileggere attentamente, please), ma concentriamoci sulle differenze tra il codice single-thread e quello multithread: sicuramente avrete notato che sono praticamente identici fino alla fase di listen(), e anche dopo le differenze sono minime: la fase di accept() adesso è in un loop, e per ogni connessione accettata (di un Client remoto) viene creato un nuovo thread. E cosa esegue il thread? Esegue la funzione connHandler() che contiene, guarda caso, il loop di recv() che nel vecchio codice era eseguito subito dopo la fase di accept(). Anche il successivo test del motivo di uscita (prematura) dal loop è contenuto in connHandler(), e mostra il corretto segnale di errore (recv() error o client disconnected, in base al codice ritornato dalla recv()).
Cosa aggiungere? Semplice e super-funzionale: un Socket Server multithread con quattro righe di codice! Ovviamente la sintassi di creazione del thread e l'esecuzione della start_routine dello stesso sono identiche a quelle descritte qui. Per testare il nostro Socket Server è necessario compilare anche un Socket Client (ovviamente quello descritto nel mio vecchio post Il Client oscuro - Il ritorno), ed eseguire, ad esempio, una istanza del Socket Server e due istanze del Socket Client (in tre terminali diversi della stessa macchina, oppure su tre macchine diverse). Eseguendo sulla mia macchina (Linux, ovviamente) su tre terminali il risultato è il seguente:
Nel terminale 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999 ./sockserver-mt: attesa connessioni entranti... ./sockserver-mt: connessione accettata ./sockserver-mt: connessione accettata connHandler: ricevuto messaggio dal sock 4: pippo connHandler: ricevuto messaggio dal sock 5: pluto connHandler: client disconnected connHandler: client disconnected
Nel terminale 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Scrivi un messaggio per il Server remoto: pippo ./sockclient: Server reply: mi hai scritto: pippo Scrivi un messaggio per il Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
Nel terminale 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Scrivi un messaggio per il Server remoto: pluto ./sockclient: Server reply: mi hai scritto: pluto Scrivi un messaggio per il Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
notare che quando uno dei Client esce (con un CTRL-C, ad esempio) il Server se ne accorge e visualizza, come previsto, client disconnected... perfetto.
Ok, con i thread abbiamo finito. Adesso cercherò di pensare a qualche nuovo interessante argomento per il prossimo post. Come sempre vi invito a non trattenere il respiro nell'attesa...
Ciao e al prossimo post!
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.
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!
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:
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!
![]() |
...ti spiego: io sono un POSIX thread e tu un C11 thread... |
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!
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.
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!
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 è:
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.
![]() |
...ti spiego: io sono il thread A e tu sei il B... |
#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.
Iscriviti a:
Post (Atom)