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.

lunedì 20 gennaio 2025

Thread Runner
come usare i thread in C - pt.3

Eldon Tyrell: La luce che arde col doppio di splendore brucia per metà tempo. E tu hai sempre bruciato la tua candela da due parti, Roy. Guardati: tu sei il figliol prodigo. Sei motivo d'orgoglio per me.
Roy Batty: Ho fatto delle cose discutibili...
Eldon Tyrell: Anche delle cose straordinarie, Roy. Godi più che puoi.
Roy Batty: Cose per cui il Dio della biomeccanica non ti farebbe entrare in paradiso.

(...una premessa: questo post è un remake di un mio vecchio post (parte 3 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Con questo post chiudiamo (in bellezza, spero) il mini-ciclo sui thread, ispirato al mitico Blade Runner del Maestro Ridley Scott.

...è qui che si scrivono applicazioni multithread?...

Dopo gli esempi base delle prime due parti del ciclo (che avete appena riletto, vero? qui e qui), è il caso di fare un esempio pratico di una delle tante applicazioni che possono usare 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 monothread. 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!

// sockserver-mt.c - un semplice socket server multithread
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>

#define BACKLOG 10 // per listen()
#define MYBUFSIZE 1024

// prototipi locali
void *connHandler(void *conn_sock);

// funzione main()
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;
}

// creo il socket in modo Network e Stream
int sock;
if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
// errore di creazione
printf("%s: non posso creare il socket (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// prepara la struttura sockaddr_in per questo server
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // set address family
server.sin_addr.s_addr = INADDR_ANY; // set server address per qualunque interfaccia
server.sin_port = htons(atoi(argv[1])); // set port number del server

// associa l'indirizzo del server al socket
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
// errore bind
printf("%s: errore bind (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// start ascolto con una coda di max BACKLOG connessioni
if (listen(sock, BACKLOG) == -1) {
// errore listen
printf("%s: errore listen (%s)\n", argv[0], strerror(errno));
close(sock);
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(sock, (struct sockaddr *)&client, &socksize)) != -1) {
printf("%s: connessione accettata dal sock %d\n", argv[0], client_sock);
if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) {
// errore pthread_create()
printf("%s: errore pthread_create (%s)\n", argv[0], strerror(errno));
close(sock);
return EXIT_FAILURE;
}
}

// errore accept()
printf("%s: errore accept (%s)\n", argv[0], strerror(errno));
close(sock);
return EXIT_FAILURE;
}

// connHandler() - funzione di connessione eseguita dai thread
void *connHandler(void *conn_sock)
{
// ottengo i dati del thread con un cast (int *) di (void *) conn_sock
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];
snprintf(server_msg, sizeof(server_msg), "mi hai scritto: %s", client_msg);
send(client_sock, server_msg, strlen(server_msg), 0);

// clear del buffer
memset(client_msg, 0, MYBUFSIZE);
}

// loop terminato: test motivo
if (read_size == -1) {
// errore recv()
printf("%s: errore recv (sock %d)\n", __func__, client_sock);
}
else {
// read_size == 0: il client si è disconnesso
printf("%s: client disconnesso (sock %d)\n", __func__, client_sock);
}

// il chiude il socket ed esce
close(client_sock);
pthread_exit(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 monothread e quello multithread: sicuramente avrete notato che sono praticamente identici fino alla fase di listen(2), e anche dopo le differenze sono minime: la fase di accept(2) 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 locale connHandler() che contiene, guarda caso, il loop di recv(2) che nel vecchio codice era eseguito subito dopo la fase di accept(2). Anche il successivo test del motivo di uscita (prematura) dal loop è contenuto in connHandler(), e mostra il corretto segnale di errore (errore recv o client disconnesso, in base al codice ritornato dalla recv(2)).

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 thread function dello stesso sono identiche a quelle descritte qui. Per testare il nostro Socket Server è necessario compilare anche un Socket Client  (ovviamente quello descritto in un altro 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:

ldo@Linux $ ./sockserver-mt 9999
./sockserver-mt: attesa connessioni entranti...
./sockserver-mt: connessione accettata dal sock 4
./sockserver-mt: connessione accettata dal sock 5
connHandler: ricevuto messaggio dal sock 4: pippo
connHandler: ricevuto messaggio dal sock 5: pluto
connHandler: client disconnesso (sock 4)
connHandler: client disconnesso (sock 5)

Nel terminale 2:

aldo@Linux $ ./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

Nel terminale 3:

aldo@Linux $ ./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

notare che quando uno dei Client esce (con un CTRL-C, ad esempio) il Server se ne accorge e visualizza, come previsto, client disconnesso... perfetto!

A questo punto, però, è doveroso aggiungere due note a margine:

1. Multitreading vs Multiplexing

Come già scrissi in altri articoli (qui, qui e qui), non necessariamente il multithreading è la scelta migliore (anzi, spesso non lo è). Ad esempio proprio sull'argomento Server TCP multithread (come quello descritto sopra), nel mio articolo sulla select(2) parlai della diatriba multithreading vs multiplexing e scrissi questo:

...Un piccolo esempio: un buon Server TCP che serve 10000 Client: secondo voi è più efficiente e funzionale aprire 10000 thread che aspettano i dati dai Client o usare il multiplexing ?...

Quindi occhio: in un programma la stessa cosa si può fare in modi molto differenti, per cui non fatevi prendere dalla fretta o dalle mode del momento e cercate sempre di dedicare del tempo alla scelta della soluzione ottimale.

2. La strerror(3)

Avrete notato che nell'esempio qui sopra ho usato la strerror(3) nelle varie segnalazioni di errore: considerando che ho già scritto in passato che nei programmi multithread  bisogna sempre usare la strerror_r(3) qualcuno potrebbe pensare che sono improvvisamente rincoglionito (potrebbe anche essere, eh!). In realtà e` successa una cosa che avevo previsto (sono un preveggente, ah ah ah), infatti avevo scritto, proprio in quell'articolo:

...non è vietato 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...

Ecco, alla fine è successo: la strerror(3) standard ora è thread-safe, infatti nelle ultime versioni dei manuali Linux della funzione c'è questa descrizione:

ATTRIBUTES
For an explanation of the terms used in this section, see attributes(7).
┌────────────────────┬───────────────┬──────────────────────────┐
│ Interface          │ Attribute     │ Value                    │
├────────────────────┼───────────────┼──────────────────────────┤
│ strerror()         │ Thread safety │ MT-Safe                  │
├────────────────────┼───────────────┼──────────────────────────┤
│ strerrorname_np(), │ Thread safety │ MT-Safe                  │
│ strerrordesc_np()  │               │                          │
├────────────────────┼───────────────┼──────────────────────────┤
│ strerror_r(),      │ Thread safety │ MT-Safe                  │
│ strerror_l()       │               │                          │
└────────────────────┴───────────────┴──────────────────────────┘
Before glibc 2.32, strerror() is not MT-Safe.

E quindi cosa bisogna fare? Io direi che valgono ancora le considerazioni che feci al tempo: adesso si può usare con discreta tranquillità la strerror(3) in un nuovo programma multithread, ma con la cautela di valutare se quel programma non finirà per essere compilato e usato su qualche sistema non molto aggiornato, eh! E soprattutto non vi passi per la testa di andare a fare refactoring di vecchi sorgenti sostituendo le strerror_r(3) con strerror(3): sarebbe un lavoro inutile e che non tiene presente le accortezze appena descritte. Meditate gente, meditate...

Ok, con i thread direi che 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!

lunedì 16 dicembre 2024

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

Eldon Tyrell: Quale sarebbe il tuo problema?
Roy Batty: La morte.
Eldon Tyrell: La morte... beh questo temo sia un po' fuori della mia giurisdizione.
Roy Batty: Io voglio più vita, padre!

(...una premessa: questo post è un remake di un mio vecchio post (parte 2 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Dove eravamo rimasti? Ah, si: nella prima parte di Blade Runner (oops... Thread Runner) avevamo introdotto l'argomento thread  partendo dalla base, e cioè dai 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...

E qui ci vuole una premessa che parte dal lato oscuro della forza (e vabbé, 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 le funzioni della libreria libpthread, ma uso diretto di costrutti del linguaggio stesso. La realizzazione finale è stata (secondo me) brillante, e i C++11 Threads sono una delle poche cose del C++11 che uso frequentemente. Certo, anche i C++11 Threads non sono esenti da difetti, e alcuni anche abbastanza gravi, eh! Come la supercazzola dei future e promise per controllare gli errori (ne ho parlato qui)... ma usandoli senza farsi troppe seghe mentali sono abbastanza programmer-friendly. Comunque già sapete cosa ne penso della brutta deriva del C++ pilotata dal committee ISO, ne ho parlato varie volte in passato...

Ma torniamo al nostro amato C: il committee ISO del C non poteva rimanere indietro, e quindi hanno pensato di fare la stessa cosa con il C11, ovvero far si che i thread siano direttamente una parte del C... e ci sono riusciti? Prima di rispondere vi anticipo una considerazione: ho una stima del committee ISO del C maggiore di quella che ho di quello del del C++ (e non ci voleva molto...), ma in questo caso devo proprio dire che non ci siamo: a seguire vedremo il 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 e immediati); 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 l problemi principali...

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

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

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

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

// prototipi locali
int 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 = mtx_init(&thdata.mutex, mtx_plain)) != thrd_success) {
// errore!
printf("%s: non posso creare il mutex (%d)\n", argv[0], error);
return 1;
}

// loop di avvio dei thread
thrd_t tid[2];
for (int i = 0; i < NUMTHREADS; i++) {
// creo un thread
if ((error = thrd_create(&tid[i], threadFunc, &thdata)) != thrd_success) {
// errore!
printf("%s: non posso creare il thread %d (%d)\n", argv[0], i, error);
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 = thrd_join(tid[i], NULL)) != thrd_success) {
// errore!
printf("%s: non posso attendere il thread %d (%d)\n", argv[0], i, error);
return 1;
}
}

// cancello il mutex di sincronizzazione
mtx_destroy(&thdata.mutex);

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

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

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

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

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

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

// 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 terminato localmente: loc_cnt=%lu\n", loc_cnt);
thrd_exit(0);
}

Come vedete il codice nuovo è praticamente identico al vecchio, mi sono limitato a usare le nuove funzioni al posto di quelle vecchie (per esempio thrd_create() invece di pthread_create(3)) , 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(3), che (per semplicità) nello scorso esempio avevo lasciato a NULL, ma che a volte può risultare utile (leggere il manuale della funzione 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 stati considerati una priorità per chi scrive i compilatori e le varie libc, quindi per molti anni dall'introduzione è stato 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à) ci ha messo una vita a supportare i nuovi thread (in realtà a causa della mancata integrazione nella glibc). Questo è un brutto segno, per niente incoraggiante: se il mondo che gira intorno ai linguaggi e ai Sistemi Operativi non approva rapidamente vuol dire che ci sono delle perplessità.

Comunque dopo tanto tempo qualcosa è arrivato, e adesso è possibile compilare i C11 Threads su Linux (con versioni recenti di GCC e con una glibc dalla v.2.28 in su), sui vari sistemi BSD, su Windows, ma non ancora su macOS (se non mi sbaglio). Aggiungo un dettaglio illuminante sulla coppia GCC/glibc: in attesa di un supporto veramente nativo, la compilazione era basata su una implementazione che era, praticamente, un wrapper che simulava i C11 Threads usando i POSIX Threads (infatti bisognava linkare la libpthread): un po' assurdo, no?

E anche se ora si può usare direttamente GCC con una glibc recente (la v.2.28 è del 2018), io ho deciso di seguire con la stessa soluzione che è stata disponibile quasi da subito (già nel 2014) e ho già usato in passato: ho usato la musl libc che è una alternativa alla glibc, ed è dotata di un wrapper per GCC (musl-gcc). musl fornisce (su Linux) il supporto completo al C11, ed è stata (credo) la prima in assoluto a fornire il supporto nativo ai C11 Thread. E come funziona tutto questo? Una volta compilato il programma si comporta correttamente, come potete vedere qui sotto:

aldo@Linux $ musl-gcc c11threads.c -o c11threads
aldo@Linux $ ./c11threads
thread partito
thread partito
thread terminato dal main: loc_cnt=8597
thread terminato dal main: loc_cnt=8597
./c11threads: thread terminati: thdata.cnt=17194

Ma il gioco vale la candela? No, per quel che mi riguarda continuerò ad usare i POSIX Threads, che uso da molti anni e rimangono il riferimento d'eccellenza. I C11 Threads sono uno strano ibrido che non rispetta la classica sintassi della glibc (ad esempio in caso di errore non ritorna -1 ma un codice di errore e, oltretutto, non aggiorna errno), e non rispetta pienamente neanche lo standard POSIX (i codici di errore non sono inseriti in una lista analizzabile con strerror(3)). E poi, come già detto sopra, le nuove funzioni sono equivalenti alle vecchie ma non abbastanza, alcune funzionalità non sono più disponibili e alcune cose semplici (come stampare il thread Id interno della libreria o le stringhe degli errori) sono molto più complicate... e, dulcis in fundo, nelle classiche pagine dei manuali Linux e POSIX non c'è ancora traccia delle nuove funzioni. Son problemi, no?

Ok, per oggi chiudo qui. Nel prossimo e ultimo episodio di Thread Runner vedremo un esempio pratico. Rimanete sintonizzati, eh!

Ciao e al prossimo post!