Nei titoli e nei testi troverete qualche rimando cinematografico (eh 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 mando avanti anche una versione in Spagnolo (e, normalmente, i post sono disponibili un po' in ritardo rispetto alla versione in Italiano...). Potete trovarlo su El arte de la programación en C. Buona lettura.

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.
Hannah strerror e le sue sorelle
(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!
#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).
...proprio l'immagine che uno si aspetta in un blog di programmazione...
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!

#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.
...ti spiego: io sono un POSIX thread e tu un C11 thread...
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!

#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.
...ti spiego: io sono il thread A e tu sei il B...
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!

#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.

sabato 8 luglio 2017

Il buono, il brutto, il VLA
come usare i Variable Length Arrays in C - pt.3

Eccoci, e, come promesso, questo mese parleremo di un parente stretto dei VLAs, ovvero della funzione alloca()... sarà un buono, un brutto o un cattivo?
ciao, sono lo spoiler di questo post!
Allora, ho aggiunto il codice al programma di test per provare la alloca(). E, per non farci mancare niente, ho aggiunto anche del codice per provare la malloc() del C++, ovvero la new (dopo il problematico test di std::vector dello scorso post era doveroso completare il discorso con con qualcosa di più prestante, mica che si dica che ce l'ho con il C++...). Quindi useremo il programma C++ dello scorso post (tanto era praticamente identico alla versione C): vi riporto nuovamente il main() e le due funzioni di test aggiunte (per ricostruire il programma completo basta consultare i due post precedenti e fare un po' di cut-and-paste). Vai col codice!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando GCC -O2
int avoid_optimization;

// prototipi locali
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testAllocaVLA(int size);
void testVectorVLA(int size);
void testNewVLA(int size);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// funzione main()
int main(int argc, char* argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // estrae iterazioni
    int iterations = atoi(argv[1]);

    // esegue test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
    runTest(iterations, &testAllocaVLA, MYSIZE, "testAllocaVLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");
    runTest(iterations, &testNewVLA, MYSIZE, "testNewVLA");

    // esce
    return EXIT_SUCCESS;
}

// funzione testAllocaVLA()
void testAllocaVLA(
    int size)       // size per alloca()
{
    int *allocavla = (int*)alloca(size * sizeof(int));

    // loop di test
    for (int i = 0; i < size; i++)
        allocavla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2
    avoid_optimization = allocavla[size / 2];
}

// funzione testNewVLA()
void testNewVLA(
    int size)       // size per new
{
    int *newvla = new int[size];

    // loop di test
    for (int i = 0; i < size; i++)
        newvla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2
    avoid_optimization = newvla[size / 2];

    delete[] newvla;
}
Come potete vedere, le due funzioni aggiunte sono perfettamente allineate stilisticamente con le altre che avevo già proposto e sono, come sempre, iper-commentate, così non devo neanche dilungarmi in spiegazioni. E i risultati del test? Vediamoli!
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 4.318492 secondi
testMallocVLA -  Tempo trascorso: 3.676805 secondi
testStackFLA  -  Tempo trascorso: 4.339859 secondi
testHeapFLA   -  Tempo trascorso: 4.340040 secondi
testAllocaVLA -  Tempo trascorso: 3.678644 secondi
testVectorVLA -  Tempo trascorso: 10.934088 secondi
testNewVLA    -  Tempo trascorso: 3.679624 secondi
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 0.746956 secondi
testMallocVLA -  Tempo trascorso: 0.697261 secondi
testStackFLA  -  Tempo trascorso: 0.696310 secondi
testHeapFLA   -  Tempo trascorso: 0.700047 secondi
testAllocaVLA -  Tempo trascorso: 0.691677 secondi
testVectorVLA -  Tempo trascorso: 1.384563 secondi
testNewVLA    -  Tempo trascorso: 0.695037 secondi
Allora, cosa si può dire? I risultati dei test dei post precedenti li abbiamo già ampliamene commentati, quindi ora possiamo solo aggiungere che: alloca() è molto veloce, visto che è, in pratica, una malloc() nello stack (e, usandola in maniera appropriata, potrebbe/dovrebbe essere la più veloce del gruppo). E la new? Beh, si comporta (come previsto) benissimo, anche perché, quasi sempre, la new usa internamente la malloc().

Va bene, la alloca() è veloce, ma lo è (solo un po' meno) anche un VLA, e questo non lo ha salvato dal essere eletto come cattivo del film. Quindi dovremo fare di nuovo una lista di pro e contro, e vedere quale parte è più pesante. Vediamo prima i pro:
  1. la alloca() è molto veloce, già che usa lo stack invece del heap.
  2. la alloca() è facile da usare, è una malloc() senza free(). La variabile allocata ha uno scope a livello di funzione, quindi rimane valida fino a quando la funzione ritorna al chiamante, esattamente come una qualsiasi variabile automatica locale (anche un VLA funziona più o meno così, ma il suo scope è a livello di blocco, non di funzione, e questo è, probabilmente, un punto a favore dei VLAs).
  3. per il motivo visto al punto 2 la alloca() non lascia in giro residui di memoria in caso di errori gravi nella attività di una funzione (e con malloc() + free() non è altrettanto facile realizzare questo). Se poi siete soliti a usare cosucce come longjmp() i vantaggi in questo senso sono grandissimi.
  4. a causa della sua implementazione interna (senza entrare in dettagli profondi) la alloca() non causa frammentazione della memoria.
Uh, che bello! E i contro?
  1. la gestione degli errori è problematica, perché non c'è maniera di sapere se alloca() ha allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita)... uh, questo è esattamente lo stesso problema dei VLAs.
  2. la alloca() non è molto portatile, visto che non è una funzione standard e il suo funzionamento/presenza dipende molto dal compilatore in uso.
  3. la alloca() è error prone (parte 1): bisogna usarla con attenzione, visto che induce, tipicamente, a errori come usare la variabile allocata quando oramai non è più valida (passarla con un return o inserirla dentro una struttura dati esterna alla funzione, per esempio)... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
  4. la alloca() è error prone (parte 2): ci sono problemi ancora più sottili da considerare nell'uso, ad esempio può risultare MOLTO pericoloso mettere una alloca() dentro un loop o in una funzione ricorsiva  (povero stack!) o in una funzione inline (che usa lo stack in una maniera che si scontra un po' con la maniera di usare lo stack della alloca())... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
  5. la alloca() è error prone (parte 3): la alloca() usa lo stack, che è normalmente limitato rispetto allo heap (specialmente negli ambienti embedded che sono molto frequentati dai programmatori C...). Quindi esaurire lo stack e provocare uno stack overflow è facile (e difficile da controllare, vedi il punto 1)... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
Va beh, conclusioni? Ci sarebbero gli estremi per dichiarare la alloca() come un altro cattivo (stessa sorte del VLA), ma, dati i notevoli pro e, soprattutto, dato che oggi sono di buon umore, la dichiareremo solo come brutto (visto lo spoiler nella figura qui sopra?). Comunque usate la alloca() con molta cautela, uomo avvisato mezzo salvato!

Ciao e al prossimo post!

domenica 4 giugno 2017

Il buono, il brutto, il VLA
come usare i Variable Length Arrays in C - pt.2

Dunque, dove eravamo rimasti? Ah si, nell'ultimo post (che avete appena riletto, vero?) avevamo approvato (con riserva) i VLAs, che sono facili da usare, utili e con ottime prestazioni, ma allora... perché ho detto che sono adatti al ruolo del cattivo nel mitico "Il buono, il brutto, il cattivo"?

non fidatevi del VLA, parola del buono!
Presto detto: oltre ai (notevoli) pro ci sono anche alcuni (pesanti) contro. Prima di seguire ricordiamoci sempre che un VLA si alloca dinamicamente nello stack come una variabile automatica con scope limitato al blocco di codice dove avviene l'allocazione: dopodiché i (principali) possibili problemi sono:
  1. la gestione degli errori è problematica, perché non c'è maniera di sapere se il VLA è stato allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita).
  2. il size del VLA si decide a run-time, quindi il compilatore deve fare dei giochi un po' strani: in base all'implementazione è possibile che una parte (anche importante) dello stack di una funzione venga riservato per un VLA, limitando molto la memoria locale disponibile. Quindi lo stack overflow è sempre dietro l'angolo.
  3. la portabilità del codice va un po' a farsi benedire: il codice diventa molto compiler-dependent e, soprattutto, visto che una buona fetta di programmatori C scrivono anche codice per sistemi embedded (dove lo stack è, spesso, limitato) risulta complicato il porting di funzioni da applicazioni normali a applicazioni embedded. Funzioni che, magari, smetterebbero di funzionare per motivi misteriosi (beh, neanche tanto misteriosi).
  4. dulcis in fundo: forse per i motivi appena elencati (o per altri ancora) da C11 in avanti i VLAs sono opzionali e subordinati a una variabile del compilatore __STDC_NO_VLA__: brutto segno.
Che fare allora? Meglio non usarli o usarli con le precauzioni del caso, anche perché le alternative non mancano. Cattivo trovato!

E adesso ci tocca cercare qualcuno che sia adatto ai ruoli del buono e del brutto. Ecco, per il buono non c'è problema, il candidato ideale è la cara, buona, vecchia malloc() che è sempre una garanzia ed è uscita molto bene dal test. Sulla malloc() è inutile dilungarci, è un punto fermo del C e ne abbiamo già parlato abbondantemente qui.

E il brutto? Beh, per cercare uno adatto dovremo, ahimè, addentrarci nel lato oscuro della forza, e cioè in territorio C++...

(apro una parentesi: non parlo mai di argomenti che non conosco, perché penso che sia stupido farlo. Per fare un esempio: io non capisco niente di moto e, vi assicuro, nessuno ha mai avuto l'onore di sentirmi disquisire sul mondiale di MotoGP. Seguo una filosofia, che, sfortunatamente, non è seguita da molta gente, è cioè: "meglio stare zitti che parlare solo per dare aria alla bocca". Proprio in virtù di questa coerenza penso di avere i titoli per parlare del C++: lo uso in parallelo al mio amato C da quasi trenta (!) anni, e, modestia a parte, penso si saperlo usare bene. Quindi ne posso disquisire, nel bene e nel male. Chiudo la parentesi).

Allora, ho ripreso pari pari l'esempio C del post precedente e (facendo il minimo sindacale di modifiche) l'ho trasformato in codice C++, per poter, così, aggiungere un test nuovo che usa std::vector (questo è un oggetto particolarmente caro ai C++ lovers, che lo usano anche per condire l'insalata). Per non ripetere tutto il codice dello scorso post vi riporto solo il main() e la nuova funzione di test aggiunta (il resto è, praticamente, invariato). Vai col codice!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando G++ -O2
int avoid_optimization;

// prototipi locali
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testVectorVLA(int size);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// funzione main()
int main(int argc, char* argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // estrae iterazioni
    int iterations = atoi(argv[1]);

    // esegue test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");

    // esce
    return EXIT_SUCCESS;
}

// funzione testVectorVLA()
void testVectorVLA(
    int size)       // size per std::vector
{
    std::vector<int> vectorvla(size);

    // loop di test
    for (int i = 0; i < size; i++)
        vectorvla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando G++ -O2
    avoid_optimization = vectorvla[size / 2];
}
(Nota: grazie alla segnalazione di Ponchietto, un lettore attento e preparato, mi sono reso conto che avevo dimenticato di aggiungere, in ogni funzione di test, una semplice istruzione che usasse l'array creato, così da evitare che l'ottimizzatore del G++ azzerasse il contenuto della funzione stessa (visto che l'array non lo usava nessuno). Ovviamente così anche i risultati dei test con ottimizzazione cambiano. Adesso il post ha codice e risultati rinnovati)

Compilando (con/senza ottimizzazioni) ed eseguendo questo codice i risultati sono i seguenti:
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 4.274441 secondi
testMallocVLA -  Tempo trascorso: 3.641508 secondi
testStackFLA  -  Tempo trascorso: 4.340430 secondi
testHeapFLA   -  Tempo trascorso: 4.312986 secondi
testVectorVLA -  Tempo trascorso: 10.660610 secondi
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 0.768702 secondi
testMallocVLA -  Tempo trascorso: 0.694418 secondi
testStackFLA  -  Tempo trascorso: 0.682241 secondi
testHeapFLA   -  Tempo trascorso: 0.694299 secondi
testVectorVLA -  Tempo trascorso: 1.364321 secondi
Come si è comportato std::vector? direi che i numeri parlano da soli... va beh, mettiamola giù in maniera diplomatica: diciamo che abbiamo due notizie, una buona e una cattiva:
  • la buona notizia è che il C++ è efficiente come il C (e su questo non avevo dubbi), infatti il nostro programma C trasformato in C++ ottiene (nei primi quattro test) le stesse prestazioni (andate a controllare la, se non ci credete, eh!).
  • la brutta notizia è che il C++ è efficiente come il C, ma solo se lo usate come il C, quindi niente STL e ammennicoli vari.
(apro un altra parentesi: ovviamente la brutta notizia qui sopra non deriva solo dal semplice test proposto in questo post: deriva da anni ed anni di osservazioni ed uso intensivo di entrambi i linguaggi, ci mancherebbe solo. Chiudo la parentesi).

Senza dilungarmi troppo (magari un giorno scriverò un post specifico sull'argomento) vi espongo la mia opinione: il C++ è un grande linguaggio potente, efficiente ed espressivo (è parente stretto del C!), con cui si può scrivere del Software di alta qualità. Ma i risultati migliori (perlomeno in termini di prestazioni e fluidità del codice) si ottengono usandolo per quello che era stato concepito originalmente, e cioè come un C a oggetti. La piega che ha preso in seguito (da quando è caduto nelle mani dei committee ISO) non mi piace e non mi convince... ma, fortunatamente (e questo è importante), continua a poter essere usato nella sua essenza, quella che permette di scrivere a oggetti usando un linguaggio (quasi) identico al C (e questo si aggancia alla buona notizia qui sopra. Ovviamente, se prestazioni e fluidità del codice non si considerano qualità importanti, allora tutte queste considerazioni perdono di significato...).

Ah, una ultima precisazione per chi si è sorpreso del codice C++ (qui sopra) che include un VLA: è una gentile offerta dal nostro amato GCC (nella sua incarnazione G++). Quindi è un estensione del linguaggio fornita dal compilatore, visto che i VLAs non fanno parte del C++ standard (neanche nelle ultime versioni C++11 e C++14).

Nel prossimo post, per chiudere il cerchio, parleremo di un parente stretto dei VLAs, e cioè della funzione alloca(). Sarà un altro buono, un altro brutto o un altro cattivo?

Ciao e al prossimo post!