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ì 14 settembre 2020

Sleep? No, grazie!
considerazioni sul perché non usare la sleep in C (e C++) - pt.1

soldato Hudson: Vengono fuori dalle pareti! Vengono fuori dalle fottute pareti!

Se ricordate, nell'articolo Sleeper - considerazioni su quale sleep usare in C avevo intrapreso la "missione" di demistificazione della sleep (e se non lo ricordate, la soluzione è semplice: andate a rileggervelo, male non vi farà). In quell'articolo avevo isolato le tre classiche domande che si fanno sull'uso delle funzioni di sleep: quale usare, come usare, quando usare.

E, per cominciare avevo cercato di rispondere alla prima, la più semplice. Avevo, poi, promesso di scrivere un articolo sulle altre due domande, che avrebbe dovuto intitolarsi, quindi, "considerazioni su come e quando usare la sleep"... ma ora, scrivendolo, mi sono reso conto che è meglio esporre qualcosa di un po' più radicale, qualcosa tipo "considerazioni sul perché NON USARE la sleep", e questo perché, come vedremo, gli usi sono (anzi, dovrebbero essere) molto limitati, anche se nella pratica (ahimè) non lo sono: ebbene si, come diceva il soldato Hudson nel bellissimo film Aliens, gli alieni (e, nel nostro caso, le sleep) "escono dalle fottute pareti!". Si usano e abusano decisamente troppo, ma non preoccupatevi, una volta letto quanto segue tutto sarà (spero) più chiaro.

...le sleep vengono fuori dalle fottute pareti!...
Ho diviso l'articolo in due parti, e anche questa volta cominceremo con la parte più semplice, descrivendo i pochi casi in cui ha senso utilizzare le istruzioni di sleep. Ovviamente, da qui in avanti, parleremo solo di programmazione multithread, perché è qui che l'argomento si fa critico, mentre nel singlethread una sleep serve solo a introdurre dei ritardi, e non credo che si possa aggiungere molto altro.

Cominciamo. Il caso più classico è quello di una applicazione modulare in cui abbiamo un flusso main() che avvia dei thread ("i moduli") che "fanno cose" in loop infinito, ossia eseguono delle operazioni e, a fine ciclo, le rieseguono. Insomma, una roba tipo quella che vi avevo mostrato nell'articolo sul watchdog (di nuovo: ricordate?), che, tra l'altro, si basava proprio sull'idea di sorvegliare dei thread di questo tipo per segnalare eventuali interruzioni impreviste, starvation, ecc. E già che ci sono vi ripropongo una parte della descrizione che scrissi:

Come funziona un Watchdog di terzo livello? Il modus operandi è 
abbastanza semplice, e include il rispetto di poche direttive di 
base:

- i thread da monitorare devono avere una struttura "classica", 
  e cioè quella di una funzione "che fa cose" in loop infinito con
  un opportuno intervallo di sleep tra un ciclo e l'altro.
- i thread da monitorare devono registrarsi al Watchdog prima di 
  avviare il loop infinito.
- nessuna delle cose che il thread fa nel loop deve essere bloccante: 
  ad esempio se si legge da un socket questo deve essere stato aperto 
  in modo nonblocking.
- ad ogni giro del loop (appena prima della sleep) si deve aggiornare 
  una variabile di monitoring che verrà letta dal Watchdog vero e 
  proprio.

Ecco, per oggi abbiamo finito, questo è l'unico caso in cui ha senso usare una istruzione di sleep...

E vabbè, dai, si può aggiungere ancora qualcosa. Cominciamo con un piccolo esempio che può essere usato come programma di test: è una versione semplificata di quello che avevo proposto per il watchdog, e ci permetterà di fare alcune (spero) interessanti considerazioni, anche per ampliare un po' il discorso su quale sleep usare. Vai col codice!

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>

// creo un nuovo tipo per passare dei dati ai thread
typedef struct _tdata {
    int  index;     // thread index
    bool *stop;     // flag per stop thread
} tdata;

// prototipi locali
void *faiCose(void *arg);
void Sleep(unsigned int milliseconds);

// main() - funzione main
int main(int argc, char* argv[])
{
    // init thread
    pthread_t tid[2];
    tdata     data[2];
    bool      stop = false;
    for (int i = 0; i < 2; i++) {
        // set data del thread e crea il thread
        data[i].index = i;
        data[i].stop  = &stop;
        int error;
        if ((error = pthread_create(&tid[i], NULL, &faiCose, (void *)&data[i])) != 0)
            printf("%s: non posso creare il thread %d (%s)\n", argv[0], i, strerror(error));
    }

    // dopo 10 secondi fermo i thread
    time_t start_time = time(NULL);
    for (;;) {
        // test timeout
        if (time(NULL) - start_time >= 10) {
            stop = true;
            break;
        }

        Sleep(100);
    }

    // join threads
    pthread_join(tid[1], NULL);
    pthread_join(tid[0], NULL);

    // exit
    printf("%s: thread terminati\n", argv[0]);
    return EXIT_SUCCESS;
}

// faiCose() - thread routine
void *faiCose(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);
    unsigned long i = 0;
    for (;;) {
        // incrementa counter
        i++;

        // test stop flag
        if (*data->stop) {
            // il thread esce
            printf("thread %d terminato dal main (i = %ld))\n", data->index, i);
            pthread_exit(NULL);
        }

        // thread sleep
        Sleep(1);
    }

    // il thread esce per altro motivo che lo stop flag
    printf("thread %d terminato localmente (i = %ld)\n", data->index, i);
    pthread_exit(NULL);
}

// Sleep() - una sleep in ms
void Sleep(unsigned int milliseconds)
{
    // testa il tempo di sleep per intercettare il valore 0
    if (milliseconds > 0) {
        // usa nanosleep() o usleep()
        #if (_POSIX_C_SOURCE >= 199309L)
        struct timespec ts;
        ts.tv_sec  = milliseconds / 1000;
        ts.tv_nsec = ( milliseconds % 1000) * 1000000;
        nanosleep(&ts, NULL);
        #else
        usleep(milliseconds * 1000);
        #endif
    }
    else {
        // usa sched_yield() come alternativa Linux alla Sleep(0) di Windows
        sched_yield();
    }
}

Allora, come avrete notato stiamo usando la famosa Sleep() multi-piattaforma che avevo proposto nell'articolo "Sleeper", e grazie a questo uso possiamo anche facilmente provare la famigerata Sleep(0) e l'effetto reale che si ottinene cambiando il valore del tempo passato come argomento: vediamo una semplice tabellina con i casi che ho analizzato su una macchina Linux con un i7 (4 core e 8 thread). Per i casi inferiori al millisecondo ho modificato il codice per usare la obsoleta (ma in questo caso comoda) usleep(), e ho registrato i valori (approssimati) di uso della CPU e del counter "i" che ci mostra quanti cicli ha effettuato ogni thread:

senza sleep         i = 5314563827  CPU = 100%
Sleep(0)            i = 18863299    CPU = 100% (esegue sched_yeld())
Sleep(10)   t=10ms  i = 900         CPU = 1/2%
Sleep(1)    t=1ms   i = 9000        CPU = 1/2%
usleep(100) t=100us i = 62970       CPU = 1/2%
usleep(10)  t=10us  i = 145000      CPU = 1/2%
usleep(1)   t=1us   i = 170000      CPU = 1/2%
usleep(0)   t=0us   i = 170000      CPU = 1/2% (test con usleep(0))

Il primo caso ci mostra perchè qui conviene usare la sleep: i nostri due thread funzionano bene (i due contatori avanzano simultaneamente), e la attività è gestita dal thread-scheduler (facile in questo caso: un thread dell'applicazione su un thread della CPU), ma comunque, se non lavorano volontariamente in maniera cooperativa (o gentile, se preferite) e non rilasciano ogni tanto la CPU, finiscono col mangiarsela tutta (due degli otto CPU-thread, nel mio caso), e in un sistema multiprocess non è una buona cosa, no?.

Esaminiamo, ora, il secondo caso che ci dimostra che la famosa Sleep(0)/sched_yeld() di cui abbiamo parlato nell'altro articolo non da proprio dei gran risultati: i thread lavorano meno (guardare il counter) ma la CPU va lo stesso al 100%.

Il terzo e il quarto caso ci mostrano che, usando valori dell'ordine di grandezza del time-slice del sistema, la CPU lavora poco e il comportamento è lineare (fino a 1 ms).

I casi successivi mantengono il rispetto della CPU ma il comportamento diventa irregolare (i counter non aumentano proporzionalmente alla riduzione della sleep). Conclusione: in questa tipo di architettura Software (che è un buon riferimento, essendo abbastanza comune) è conveniente usare dei tempi paragonabili al time-slice del sistema, ossia tra 1 e 10 ms.

L'ultimo caso l'ho aggiunto solo per curiosità, per confermare quanto detto nell'altro articolo: usleep(0) non è equivalente a Sleep(0) (...magari qualcuno non ci credeva...), e applica il tempo di sleep minimo possibile, che è di 1 us.

E con questo abbiamo finito veramente, perché la sleep serve veramente a poco: sincronizzare i thread di una applicazione mandandone a dormire qualcuno non è proprio la maniera di operare più ottimizzata. I thread dovrebbero lavorare/fermarsi/riattivarsi in base a eventi, mentre affidarsi a intervalli di tempo (spesso scelti in maniera arbitraria) non è proprio una ideona... specialmente considerando che, una sleep con un tempo x garantisce che il thread stia fermo come minimo per quel tempo x, ma non garantisce il tempo massimo, che potrebbe essere anche molto più grande, in base a quanto è affollata la coda dei runnable-thread in attesa di partire.

E, come dicono oltreoceano, quando trovate una applicazione multithread piena di sleep ("che escono dalle fottute pareti!") siamo in presenza di un caso di "poor design", che necessita assolutamente di essere migliorato. Ma tutto questo lo vedremo nella seconda parte dell'articolo, per oggi può bastare. Mettetevi in sleep che al momento opportuno vi risveglierò io...

Ciao, e al prossimo post!

domenica 16 agosto 2020

Prototipi? Si, grazie!
considerazioni sull'uso dei prototipi in C (e C++)

(...una premessa: questo post è un remake di un mio vecchio post. Ma, anche se tratta lo stesso argomento, amplia notevolmente il discorso è affronta anche altri temi. Leggete e mi direte...

Dante: Dunque un modo per aprirla è quello della dinamite. Sistema che usava il famoso fu Cimin.
Tiberio: Fu Chi Min? Chi è, un cinese?
Dante: Ma che cinese! Veneziano era! “Fu” sarebbe che morì, Cimin è il cognome, no?!

Questo surreale dialogo tra Dante Cruciani (Totò) e Tiberio Braschi (Marcello Mastroianni) è tratto dal bellissimo I soliti ignoti, che è considerato (a ragione) uno dei prototipi della grande Commedia all'italiana. Ma cos'è un prototipo? Uhm... "Primo esemplare, modello originale di una serie di realizzazioni successive..." [Cit. Treccani]. Ecco, oggi parleremo dei Prototipi di Funzione nel linguaggio C (con qualche incursione nel C++) che sono molto somiglianti alla definizione del dizionario.

...facce da: "cosa sono sti' prototipi?"...
Dopo una rapida ispezione in rete ho notato una certa confusione sull'argomento. Prototipi obbligatori, forse consigliati, a volte sconosciuti... ho notato informazioni fuorvianti perfino in dispense universitarie (ahi, ahi). Tra l'altro, nei miei trascorsi, ho incontrato anche colleghi programmatori che non avevano le idee chiare sull'argomento. Beh, allora è giunta l'ora di fare chiarezza!

Partiamo dai dati di fatto, lasciando alla seconda parte del post le considerazioni tecniche/filosofiche sull'argomento. Mi raccomando di prestare attenzione, nel seguito del testo, ad alcune parole chiave che useremo e cercheremo di illustrare: prototipo, dichiarazione e definizione. E, faremo riferimento anche alle varie versioni del C che ci hanno accompagnato fino ad oggi che, in ordine di tempo, sono: K&R C, ANSI C (C89/C90) e C99/C11. Se non altrimenti specificato tutte le prossime affermazioni/considerazioni si riferiranno alle versioni più recenti, C99 e C11 (che non hanno, tra di loro, differenze significative su questo argomento).

Veniamo al dunque: nel C i prototipi non sono obbligatori. La confusione su questo fatto deriva dalla doppia personalità che hanno molti programmatori C (incluso il sottoscritto) che devono, spesso, districarsi tra C e C++ facendo, a volte, un po' di confusione: i prototipi sono obbligatori nel C++, per motivi strettamente collegati ad alcune funzionalità del linguaggio (vi suona il Function Overloading?).

Nel C, invece, è obbligatoria la dichiarazione di una funzione.

Facciamo, allora, un esempio sulle parole chiave dichiarazione, prototipo e definizione, usando solo una sintassi di tipo moderno (ANSI C o C99/C11):

// dichiarazione di funzione (valida ma sconsigliata perché ambigua e obsoleta)
int myFunc();

// dichiarazione di funzione con prototipo
int myFunc(int val):

// definizione di funzione con prototipo
int myFunc(int val)
{
    int retval;

    // faccio cose
    ...

    return retval;
}
L'ordine nell'esempio descritto, come evidente, non è casuale: la dichiarazione è il caso elementare, mentre il prototipo contiene implicitamente una dichiarazione e, infine, la definizione contiene implicitamente un prototipo (e quindi anche una dichiarazione). E, dato che ci siamo, aggiungiamo, per completezza, le sintassi di definizione permesse ma troppo old-fashioned, e le sintassi vietate da C99/C11:
// definizione con dichiarazione "old style" senza prototipo
int myFunc(val)
    int val;
{
    // faccio cose
    ...

    return 0;
}

// definizione implicitamente dichiarata:
// equivale a "int myFunc(int val)" (non permessa nel C99)
myFunc(val)
{
    // faccio cose
    ...

    return 0;
}
Prima di passare alla parte filosofica, facciamo una breve analisi storica: nel K&R C non c'era l'obbligo di dichiarazione delle funzioni, quindi non c'era nessun controllo a compile-time sul valore di ritorno e, ancor meno, sulla coerenza dei parametri passati: in mancanza della dichiarazione il compilatore applicava un comportamento di default e assumeva che la funzione ritornava un int. Per i parametri si applicava la default argument promotion: gli interi venivano promossi a int, e i float erano promossi a double.

Con l'avvento del ANSI C (o C89/C90), sono arrivati i prototipi, però è stata mantenuta la retro-compatibilità con la vecchia sintassi (per non obbligare a sistemare milioni di linee di codice funzionante). Con questa novità era, finalmente, possibile controllare a compile-time la correttezza d'uso delle funzioni, sia sui parametri che sui valori di ritorno. A causa della retro-compatibilità rimaneva, però, possibile scrivere nuovo codice con la sintassi antica, ed, inoltre, rimaneva valido il concetto del default return value in assenza di dichiarazione.

Con il C99 si è fatto un ulteriore passo in avanti: va bene la ricerca della compatibilità con il codice pre-esistente, ma il valore di ritorno di default era una falla troppo grande nella solidità del linguaggio, per cui si è introdotta la dichiarazione obbligatoria, come indicato all'inizio del post (aggiungo che si è anche reso obbligatorio l'uso dei prototipi negli standard headers del linguaggio, ma questa è un altra storia...).

E ora, dopo avere descritto quello che lo standard ci obbliga e/o permette di fare, veniamo, finalmente, a ciò che è meglio fare: secondo me un buon programmatore usa i prototipi (quindi, presumo, per la proprietà transitiva chi non usa i prototipi non è un buon programmatore. Ho detto presumo, quindi se qualcuno si è offeso non se la prenda con me, se la prenda con la proprietà transitiva). E perché consiglio così caldamente l'uso dei prototipi? Beh, il C è un linguaggio tipizzato, per cui è così evidente l'aiuto che questo meccanismo ci può dare per produrre codice senza errori di tipo, migliorando al tempo stesso leggibilità e manutenibilità, che non c'è neanche bisogno di spiegarlo!

E, per aggiungere un tocco di radicalità che non guasta mai, aggiungo che, per le suddette questioni di leggibilità e manutenibilità del software, non è conveniente affidarsi al fatto che, usando definizioni con prototipo (vedi esempio sopra), e scrivendo il codice nel giusto ordine (cioè usando una funzione solo dopo la sua definizione), non è necessario scrivere dei veri e propri prototipi. Non siate pigri nelle cose utili, per favore!

E come deve essere strutturato un buon codice rispetto a quanto detto sopra? Vediamo un esempio di struttura elementare con tre file:

  1. un header-file che contiene i prototipi globali.
  2. un library-file che include l'header del punto 1 e contiene le definizioni (con prototipo) delle funzioni prototipate nel header-file.
  3. un implementation-file che include l'header del punto 1, e usa le funzioni prototipate nel header-file. L'implementation-file contiene, ovviamente, anche i prototipi delle eventuali funzioni locali e le relative definizioni (con prototipo).

E con questo sarebbe tutto, anche se possiamo aggiungere una interessante curiosità un po' OT, ma che merita un approfondimento per evitare equivoci. Vediamo di cosa stiamo parlando:

// due dichiarazioni in C
int myFunc1();      // funzione con numero arbitrario di argomenti
int myFunc2(void);  // funzione con 0 argomenti

// due dichiarazioni in C++
int myFunc1();      // funzione con 0 argomenti
int myFunc2(void);  // funzione con 0 argomenti
Avete letto i commenti? Attenzione, quindi! In C int myFunc1() e int myFunc2(void) sono funzioni diverse, mentre in C++ significano la stessa cosa. Quindi occhio a non fare (in C) stranezze tipo queste:
// definizione con dichiarazione "old style" senza prototipo
int myFunc1()
{
    // faccio cose
    ...

    return 0;
}

// definizione di funzione con prototipo
int myFunc2(void)
{
    // faccio cose
    ...

    return 0;
}

// funzione main
main()
{
    ...
    myFunc1(1, 2);  // NOK: "undefined behaviour" e nessun warning/errore in compilazione
    myFunc2(1, 2);  // OK:  errore in compilazione per uso improprio della funzione
    ...
}
Quindi, usando impropriamente una funzione con dichiarazione old-style (come la myFunc1() qui sopra) si produce un undefined behaviour (come ci conferma lo standard del C99) che si potrebbe trasformare in qualche mal di testa...

Ecco, quando passate frequentemente da C a C++ (e viceversa) ricordatevi di queste cose (e di alcune altre, ma questa è un altra storia...). Comunque, una cosa è sicura: le dichiarazioni old-style sono state mantenute per retro-compatibilità, ma non usatele mai, per favore!

E ho detto tutto!

Ciao, e al prossimo post!

domenica 19 luglio 2020

Sleeper
considerazioni su quale sleep usare in C

(...una premessa: questo post potrebbe sembrare un remake di un mio vecchio post. In realtà tratta, incidentalmente, lo stesso argomento, ma amplia notevolmente il discorso è affronta ben altri temi. Leggete e mi direte...)
Luna: Il sesso è diverso oggi... vedi, noi non abbiamo nessun problema: tutti sono frigidi.
Miles: Oh ma è incredibile: gli uomini sono impotenti?
Luna: Oh sì, la maggior parte... eccetto, sì, quelli che sono di discendenza italiana.
Miles: Doveva essere qualcosa negli spaghetti!
Questo post è stato a lungo dormiente, esattamente come il Miles (Woody Allen) di Sleeper, che ha dormito 200 anni e si è risvegliato, come dire? Un po' spaesato in un epoca che non era la sua. Un post dormiente (come l'argomento) che ho deciso di svegliare perché ho accumulato letture di articoli, di quesiti (su stackoverflow) e di codice vario, che dimostrano l'esistenza di molteplici dubbi sull'uso delle funzioni di sleep: quale usare, come usare, quando usare. Ecco, in questo articolo cercherò di dare qualche spunto sulla prima (e più semplice) domanda (quale usare?). E sto già scrivendo un "considerazioni su come e quando usare la sleep", che mi costerà un po' ultimare perché è un argomento abbastanza complesso... dovrete pazientare un po'.
 
Sleeper
...te l'avevo detto di non svegliarmi...

Vediamo ora le più importanti opzioni a disposizione: sleep(), usleep(), nanosleep() sui sistemi POSIX e Sleep() su Windows. Vediamole un po' più in dettaglio, perché ognuna ha i suoi pro e contro:
  • sleep(): è Ok ma ha una risoluzione (in secondi) troppo bassa: un secondo per alcuni usi è una eternità. Ha alcune criticità rispetto ai segnali.
  • usleep(): è obsoleta: è stata bannata dagli standard POSIX perché ha una risposta ai segnali indefinita.
  • nanosleep(): è Ok: è ad alta risoluzione (in nanosecondi) e usa correttamente i segnali, ma ha una interfaccia un poco ostica...
  • Sleep(): è Ok e ha una buona risoluzione (in millisecondi) ma è solo per Windows e crea possibili problemi di porting, come vedremo più avanti.
Analizziamo per punti, saltando però la funzione sleep(): noi vogliamo analizzare l'uso della sleep come sistema di sincronizzazione nella programmazione multithread (anche se, come vedremo nel prossimo articolo, spesso non è un uso raccomandabile), quindi necessitiamo alta risoluzione (millisecondi o meno, invece dei secondi della sleep()). Vai col punto 1!

1. Lo scontro del secolo: usleep() vs nanosleep()

La funzione usleep() è stata deprecata nello standard POSIX dal 2001 ed è stata rimossa nel 2008. Per questo motivo, è consigliabile utilizzare la funzione nanosleep(), che non è obsoleta e presenta numerosi vantaggi, tra cui il comportamento relativo ai segnali:
dalla usleep() Linux Man Page:
"...4.3BSD, POSIX.1-2001.  POSIX.1-2001 declares this function obsolete; use 
nanosleep(2) instead. POSIX.1-2008 removes the specification of usleep()..."
dalla nanosleep() Linux Man Page:
"...it provides a higher resolution for specifying the sleep interval; POSIX.1 
explicitly specifies that it does not interact with signals; and it makes the 
task of resuming a sleep that has been interrupted by a signal handler easier..."
Però, come detto sopra, la nanosleep() ha un interfaccia un poco ostica, visto che "parla in nanosecondi" e oltretutto usa una struttura di appoggio per settare i tempi. Visto che a noi interessa semplificare e usare una risoluzione in millisecondi, possiamo utilizzare un semplice wrapper come questo che ho scritto (notare che ho chiamato Sleep() il wrapper, per motivi che chiarirò più avanti):
void Sleep(unsigned int milliseconds)
{
    // usa nanosleep() o usleep()
    #if (_POSIX_C_SOURCE >= 199309L)
    struct timespec ts;
    ts.tv_sec  = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000;
    nanosleep(&ts, NULL);
    #else
    usleep(milliseconds * 1000);
    #endif
}
Faccio notare che, nelle versioni più recenti della  glibc (e anche nella ottima musl) il wrapper è quasi già integrato: dato che usleep(), pur essendo deprecata, è ancora usata in molti programmi, per portabilità viene implementata (più o meno) così:
int usleep(useconds_t useconds)
{
    struct timespec ts = {
        .tv_sec = useconds/1000000,
        .tv_nsec = (useconds%1000000)*1000
    };

    return nanosleep(&ts, NULL);
}
E, non ci crederete, ma nelle versioni più recenti di glibc anche la sleep() è implementata usando la nanosleep(), che si dimostra il vero e proprio prezzemolino delle funzioni di questo tipo!

E adesso che abbiamo a disposizione un bel wrapper è il momento dei parlare di (possibili) problemi di portabilità. Vai col punto 2!

2. Il mistero della Sleep(0)

Ho usato per il wrapper qui sopra una sintassi simile alla Sleep() di Windows, proprio per evidenziare un possibile uso nella scrittura di Software "portabile" (o meglio: multi-piattaforma). Quindi, per questo nuovo uso, possiamo migliorare il wrapper nella maniera seguente:
#ifndef WIN32   // ridefinisce la Sleep() solo in ambiente POSIX
void Sleep(unsigned int milliseconds)
{
    // usa nanosleep() o usleep()
    #if (_POSIX_C_SOURCE >= 199309L)
    struct timespec ts;
    ts.tv_sec  = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000;
    nanosleep(&ts, NULL);
    #else
    usleep(milliseconds * 1000);
    #endif
}
#endif
In questa maniera se cerchiamo di usare lo stesso codice su sistemi POSIX o su Windows useremo automaticamente la funzione opportuna (ossia: il wrapper o quella di sistema) senza modificare il codice. Ma ci sono alcune problematiche da tenere in conto: la sleep di Windows non è interrompibile dai segnali (che su Windows non ci sono) e, soprattutto, è possibile usare Sleep(0). E a cosa serve una istruzione di questo tipo? Vediamo cosa dice il manuale:
dalla Sleep() MSDN Doc Page:
"...A value of zero causes the thread to relinquish the remainder of its time 
slice to any other thread that is ready to run. If there are no other threads 
ready to run, the function returns immediately, and the thread continues execution..."
Ossia, Sleep(0) non tiene a dormire il thread chiamante come minimo per i millisecondi impostati (che è il comportamento classico), ma invece, praticamente, permette a eventuali thread in attesa di avviarsi e, se non ce ne sono in attesa, il thread chiamante riprende il funzionamento senza nessun ritardo. Faccio notare che, in realtà, Windows ha una funzione ad-hoc per questo, la SwitchToThread(), ma è abbastanza usuale usare Sleep(0). E faccio anche notare che Sleep(0) è una istruzione da usare con molta parsimonia, perché ha molti effetti collaterali se non è usata nei punti giusti del codice (aumento esagerato dei thread context switch, aumento dell'uso di CPU, etc.).

Comunque, tornando al nostro wrapper, voi mi direte: "e quale è il problema? Il wrapper chiamerà usleep o nanosleep con un tempo zero, quindi problema risolto!". E invece no, perché, sfortunatamente, alcune implementazioni UNIX/Linux delle varie funzioni di sleep non hanno, con un "time to sleep zero", un comportamento equivalente a quello della Sleep(0). Anzi, in alcuni casi (come la sleep(), ad esempio) il codice inizia con qualcosa del genere:
if (time_to_sleep == 0)
    return;
Quindi la nostra Sleep(0) sui sistemi POSIX non è completamente compatibile con quella dei sistemi Windows... Che si fa allora? Ma è semplice, ridefiniamo ancora il nostro wrapper come segue:
#ifndef WIN32   // ridefinisce la Sleep() solo in ambiente POSIX
void Sleep(unsigned int milliseconds)
{
    // testa il tempo di sleep per intercettare il valore 0    
    if (milliseconds > 0) {
        // usa nanosleep() o usleep()
        #if (_POSIX_C_SOURCE >= 199309L)
        struct timespec ts;
        ts.tv_sec  = milliseconds / 1000;
        ts.tv_nsec = ( milliseconds % 1000) * 1000000;
        nanosleep(&ts, NULL);
        #else
        usleep(milliseconds * 1000);
        #endif
    }
    else {
        // usa sched_yield() come alternativa Linux alla Sleep(0) di Windows 
        sched_yield();
    }
}
#endif
La funzione sched_yield() è l'equivalente POSIX di Sleep(0) e SwitchToThread() su Windows: e anche questa bisogna usarla con attenzione perché può avere effetti collaterali (sul carico della CPU, etc.) ma, poiché sono gli stessi effetti collaterali della versione Windows, si può ragionevolmente supporre che sia possibile effettuare una sostituzione diretta (ottenendo, spesso, gli stessi risultati disastrosi).
dalla sched_yield() Linux Man Page:
"...sched_yield() causes the calling thread to relinquish the CPU. The thread is
moved to the end of the queue for its static priority and a new thread gets to run..."
E per oggi può bastare. Nell'articolo "considerazioni su come e quando usare la sleep" (che non sarà necessariamente il prossimo post) entreremo molto più in profondità sull'argomento. E faremo un confronto con gli altri metodi di sincronizzazione dei thread e si capirà che, a parte alcuni casi, non è una buona idea scrivere codice che usa molto le funzioni di sleep. Pazienza e non trattenete il respiro nell'attesa!

Ciao, e al prossimo post!

lunedì 22 giugno 2020

C'era una volta un... Allarme
come scrivere un Alarm Manager in C - pt.2

Bruce Lee: C'è un elemento di vero combattimento. Se non lo abbatti... ti uccide.
Cliff Booth: Non se Rick Dalton ha un fucile a pompa.
Ed eccoci alla seconda puntata di C'era una volta a... Hollywood, anzi no: C'era una volta un... Allarme. Cliff"Pitt"Booth, sferzante come suo solito, dice la sua sui metodi di difesa. Beh, noi siamo meno radicali e ci difendiamo con il nostro Alarm Manager (o Alarmer o Gestore di Allarmi, come preferite), la nostra arma vincente contro le situazioni critiche delle applicazioni. Con questo articolo mostreremo l'attesa implementazione delle funzioni di gestione degli allarmi, quelle già intraviste nel header file presentato nella prima parte (a proposito, spero che l'abbiate già letta se no vi faccio punire da Bruce Lee).
...te lo do io l'allarme...
Riepiloghiamo: nella prima parte abbiamo visto le strutture dati implicate nella gestione, e i prototipi delle funzioni implementate. Poi abbiamo visto un semplice esempio d'uso, ossia una applicazione modulare con un main() che lanciava due moduli che correvano in thread separati. I moduli, erano gli utilizzatori finali del Alarmer e, all'avvio, montavano i propri allarmi (cioè li registravano al gestore), per poi usarli in seguito quando necessario. Questa struttura, basata sul montaggio di gruppi di allarmi indipendenti (ogni modulo ha i suoi) è, direi, molto interessante, perché è veramente dinamica: un modulo si registra, si mette al lavoro, usa gli allarmi e, nel caso debba terminare l'attività, può "smontare" i propri allarmi prima di uscire, liberando spazio nella tabella.

Ok, bando alle ciance, so che siete impazienti di vedere l'implementazione delle funzioni... Vai col codice!
#include "alarmer.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

// prototipi locali
static Alarm *findFirstActiveAlarm(Alarmer *alarmer);
static Alarm *findNextActiveAlarm(Alarmer *alarmer);
static Alarm *findFirstHistAlarm(Alarmer *alarmer);
static Alarm *findNextHistAlarm(Alarmer *alarmer);

// setAlarmer - set valori iniziali alarmer
int setAlarmer(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // init mutex
    int error;
    if (  (error = pthread_mutex_init(&alarmer->onoff_mutex, NULL)) != 0 ||
          (error = pthread_mutex_init(&alarmer->mount_mutex, NULL)) != 0 ||
          (error = pthread_mutex_init(&alarmer->add_mutex,   NULL)) != 0   ) {

        // errore fatale: fermo la inizializzazione
        printf("%s: non posso creare il mutex (%s)\n", __func__, strerror(error));
        return -1;
    }

    // reset pointer della tabella allarmi
    for (int i = 0; i < MAX_ALARMS; i++) {
        // set pointer a 0
        alarmer->alarms_table[i] = 0;
    }

    // reset della tabella allarmi storici
    for (int i = 0; i < MAX_HIST_ALARMS; i++) {
        // reset terminatore di lista
        alarmer->hist_alarms_table[i].id = -1;
    }

    // reset dati per gestione circolare allarmi storici
    alarmer->hist_index     = 0;
    alarmer->num_hist_found = 0;
    alarmer->restart_hist   = false;

    // esco con Ok
    return 0;
}

// mountAlarms - monta un gruppo di allarmi
void mountAlarms(
    Alarmer     *alarmer,   // struttura del Alarmer
    const Alarm *alarms)    // gruppo di allarmi da montare
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->mount_mutex);

    // loop sul gruppo per aggiungere gli allarmi alla tabella
    for (int i = 0; alarms[i].id != -1; i++)
        addAlarm(alarmer, alarms[i].id, alarms[i].owner, alarms[i].fmt);

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->mount_mutex);
}

// addAlarm - aggiunge un allarme nella tabella allarmi
void addAlarm(
    Alarmer    *alarmer,    // struttura del Alarmer
    int        id,          // ID allarme
    int        owner,       // owner allarme
    const char *fmt)        // testo base allarme (formato printf)
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->add_mutex);

    // loop sopra la tabella allarmi per cercare la prima posizione libera
    for (int i = 0; i < MAX_ALARMS; i++) {
        // verifica se la posizione è libera
        if (alarmer->alarms_table[i] == 0) {
            // aggoiunge l'allarme alla tabella
            alarmer->alarms_table[i] = malloc(sizeof(Alarm));

            // set valori
            alarmer->alarms_table[i]->id       = id;
            alarmer->alarms_table[i]->owner    = owner;
            strcpy(alarmer->alarms_table[i]->fmt, fmt);
            alarmer->alarms_table[i]->active   = false;
            alarmer->alarms_table[i]->on_time  = 0;
            alarmer->alarms_table[i]->off_time = 0;

            // unlock della funzione
            pthread_mutex_unlock(&alarmer->add_mutex);

            // fermo la ricerca ed esco
            return;
        }
    }

    // segnalo errore
    printf("%s: errore: nessuna posizione disponibile\n", __func__);

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->add_mutex);
}

// onAlarm - attiva un allarme
void onAlarm(
    Alarmer *alarmer,       // struttura del Alarmer
    int     id,             // ID allarme
    int     owner,          // owner allarme
    ...)                    // lista variabile di argomenti
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->onoff_mutex);

    // loop sopra la tabella allarmi per cercare l'allarme
    for (int i = 0; i < MAX_ALARMS; i++) {
        // test owner/id (solo per allarmi in uso)
        if (    alarmer->alarms_table[i] &&
                alarmer->alarms_table[i]->owner == owner &&
                alarmer->alarms_table[i]->id == id) {

            // allarme trovato: test se già attivo
            if (alarmer->alarms_table[i]->active == false) {
                // set allarme a ON
                alarmer->alarms_table[i]->active  = true;
                alarmer->alarms_table[i]->on_time = time(NULL);

                // compone il testo con gli argomenti multipli
                va_list arglist;
                va_start(arglist, owner);
                char alarmstr[TEXT_SIZE];
                vsnprintf(alarmstr, sizeof(alarmstr), alarmer->alarms_table[i]->fmt,
                          arglist);
                va_end(arglist);

                // copia il testo e interrompe il loop
                strcpy(alarmer->alarms_table[i]->text, alarmstr);
                break;
            }
            else {
                // allarme trovato ma già attivo: interrompe il loop
                break;
            }
        }
    }

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->onoff_mutex);
}

// offAlarm - disattiva un allarme
void offAlarm(
    Alarmer *alarmer,       // struttura del Alarmer
    int     id,             // ID allarme
    int     owner)          // owner allarme
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->onoff_mutex);

    // loop to find alarm
    for (int i = 0; i < MAX_ALARMS; i++) {
        // test owner/id (solo per allarmi in uso)
        if (    alarmer->alarms_table[i] &&
                alarmer->alarms_table[i]->owner == owner &&
                alarmer->alarms_table[i]->id == id) {

            // allarme trovato: test se già attivo
            if (alarmer->alarms_table[i]->active == true) {
                // set allarme a OFF
                alarmer->alarms_table[i]->active   = false;
                alarmer->alarms_table[i]->off_time = time(NULL);

                // restart indice allarmi storici se viene raggiunto MAX_HIST_ALARMS
                if (alarmer->hist_index >= MAX_HIST_ALARMS) {
                    alarmer->hist_index   = 0;
                    alarmer->restart_hist = true;
                }

                // inserisce l'allarme nella tabella allarmi storici
                alarmer->hist_alarms_table[alarmer->hist_index] =
                        *alarmer->alarms_table[i];

                // set indice per il buffer circolare degli allarmi storici e break loop
                alarmer->hist_index++;
                break;
            }
            else {
                // allarme trovato ma già OFF: break loop
                break;
            }
        }
    }

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->onoff_mutex);
}

// getAlarms - compone una stringa con tutti gli allarmi attivi
char* getAlarms(
    Alarmer *alarmer,       // struttura del Alarmer
    char*   dest,           // stringa destinazione
    size_t  size)           // size della stringa destinatione
{
    // loop sopra la tabella allarmi per costruire la stringa destinazione
    dest[0] = 0;
    Alarm* active_alarm = findFirstActiveAlarm(alarmer);
    while (active_alarm != NULL) {
        // ricavo il campo on_time
        tzset();    // questo serve se usiamo localtime_r() invece di localtime()
        struct tm tmp;
        char on_time[32];
        strftime(on_time, sizeof(on_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&active_alarm->on_time, &tmp));

        // aggiunge un allarme alla stringa destinazione
        char* my_dest = malloc(sizeof(char[size]));
        snprintf(my_dest, size, "%s%-2d  %-2d  %-80s  %s\n", dest, active_alarm->id,
                 active_alarm->owner, active_alarm->text, on_time);
        snprintf(dest, size, "%s", my_dest);
        free(my_dest);

        // cerca il prossimo allarme attivo
        active_alarm = findNextActiveAlarm(alarmer);
    }

    // ritorna la stringa composta
    return dest;
}

// getHistAlarms - compone una stringa con tutti gli allarmi storici
char* getHistAlarms(
    Alarmer *alarmer,       // struttura del Alarmer
    char*   dest,           // destination string
    size_t  size)           // size of destination string
{
    // loop sopra la tabella allarmi per costruire la stringa destinazione
    dest[0] = 0;
    Alarm* hist_alarm = findFirstHistAlarm(alarmer);
    while (hist_alarm != NULL) {
        // ricavo i campi on_time e off_time
        tzset();    // questo serve se usiamo localtime_r() invece di localtime()
        struct tm tmp;
        char on_time[32];
        strftime(on_time, sizeof(on_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&hist_alarm->on_time, &tmp));
        char off_time[32];
        strftime(off_time, sizeof(off_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&hist_alarm->off_time, &tmp));

        // aggiunge un allarme alla stringa destinazione
        char* my_dest = malloc(sizeof(char[size]));
        snprintf(my_dest, size, "%s%-2d  %-2d  %-80s  %s  %s\n", dest,
                 hist_alarm->id, hist_alarm->owner, hist_alarm->text, on_time, off_time);
        snprintf(dest, size, "%s", my_dest);
        free(my_dest);

        // cerca il prossimo allarme storico
        hist_alarm = findNextHistAlarm(alarmer);
    }

    // ritorna la stringa composta
    return dest;
}
Il codice è, come al solito, super-commentato, quindi se lo avete letto con attenzione dovreste già conoscerne tutti i segreti. Come avrete notato abbiamo una funzione di set del Alarmer, la setAlarmer() che è quella che si usa nel main() per inizializzare la struttura base. Poi abbiamo la funzione di mount che serve a montare gli allarmi e che dovrebbe essere la prima attività eseguita da un modulo. Poi abbiamo la funzione di add che aggiunge allarmi alla Alarm Table: la usa internamente solo la funzione di mount nell'implementazione, quindi potrebbe non essere resa pubblica ma, in realtà, usi più sofisticati del nostro Alarm Manager prevedono che si possano aggiungere e togliere allarmi dinamicamente, esternamente alle operazioni di mount, quindi ha senso lasciarla pubblica.

E poi abbiamo le due funzioni che consentono di alzare e abbassare un allarme (ossia: attivare e disattivare) e che sono la onAlarm() e la offAlarm(). Queste funzioni sono quelle che permettono ai moduli utilizzatori di comunicare che c'è un problema o che è stato risolto un problema.

E, infine, abbiamo due funzioni "estetiche", getAlarms() e getHistAlarms(), che ci permettono di leggere, in forma di testo,  la situazione corrente degli allarmi, attivi e storici. Nell'esempio proposto nello scorso articolo venivano usate nel main() per presentare graficamente (a tempo) la situazione dell'esecuzione.

Sicuramente avrete notato la natura  thread-safe dell'implementazione: abbiamo un unico Alarmer, creato nel main thread, e vari moduli utilizzatori in thread separati: le varie attività devono essere quindi protette per evitare situazioni incoerenti. Per cui abbiamo alcuni mutex (creati dalla setAlarmer()) che vengono usati opportunamente per sincronizzare le operazioni.

Uhm... manca qualcosa... ah, si! Le funzioni statiche! I più attenti avranno notato quattro prototipi di funzioni statiche (ossia: che non vengono esportate) che si usano solo internamente all'implementazione del Alarmer: sono le funzioni usate da getAlarms() e getHistAlarms() per scorrere le tabelle e estrarre gli allarmi. Le quattro funzioni usano il classico metodo findfirst + findnext per eseguire il compito, e sono veramente molto semplici. Vai col codice!
// findFirstActiveAlarm - trova il primo allarme attivo nella tabella allarmi
static Alarm* findFirstActiveAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // reset indice
    alarmer->find_active_index = 0;

    // cerca il prossimo allarme attivo
    return findNextActiveAlarm(alarmer);
}

// findNextActiveAlarm - trova il prossimo allarme attivo nella tabella allarmi
static Alarm* findNextActiveAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // loop per cercare il prossimo allarme attivo
    while (alarmer->find_active_index < MAX_ALARMS) {
        // test se attivo (solo sugli allarmi in usos)
        if (    alarmer->alarms_table[alarmer->find_active_index] &&
                alarmer->alarms_table[alarmer->find_active_index]->active) {

            // incrementa indice e ritorna l'allarme trovato
            alarmer->find_active_index++;
            return alarmer->alarms_table[alarmer->find_active_index - 1];
        }

        alarmer->find_active_index++;
    }

    // ritorna allarme non trovato
    return NULL;
}

// findFirstHistAlarm - trova il primo allarme nella tabella allarmi storici
static Alarm* findFirstHistAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // reset contatore e indice
    alarmer->num_hist_found = 0;
    if (!alarmer->restart_hist)
        alarmer->find_hist_index = 0;
    else
        alarmer->find_hist_index = alarmer->hist_index;

    // cerca il prossimo allarme storico
    return findNextHistAlarm(alarmer);
}

// findNextHistAlarm - trova il prossimo allarme nella tabella allarmi storici
static Alarm* findNextHistAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // restart indice allarmi storici se viene raggiunto MAX_HIST_ALARMS
    if (alarmer->find_hist_index >= MAX_HIST_ALARMS)
        alarmer->find_hist_index = 0;

    // cerca il prossimo allarme storico
    if (    alarmer->num_hist_found < MAX_HIST_ALARMS &&
            alarmer->hist_alarms_table[alarmer->find_hist_index].id != -1) {

        // incrementa contatore e indice e ritorna l'allarme storico trovato
        alarmer->num_hist_found++;
        alarmer->find_hist_index++;
        return &alarmer->hist_alarms_table[alarmer->find_hist_index - 1];
    }

    // ritorna allarme storico non trovato
    return NULL;
}
Come giá anticipato nella prima parte del post, ho omesso alcune funzioni e funzionalità, per snellire un po' la presentazione del codice, quindi mancano la delAlarmer(), la umountAlarm() e la delAlarm(): sono funzioni speculari, rispettivamente, della setAlarmer(), della mountAlarm() e della addAlarm(), e non sono molto complicate da scrivere: fanno esattamente il contrario delle funzioni corrispondenti.

Un capitolo a parte merita il discorso offset e instance che sono commentati nella mountAlarms(), quindi vi ripeto quello che scrissi nella prima parte:

...l'operazione di mount inserisce gli allarmi nelle posizioni opportune della tabella interna, e può gestire anche "gli stessi allarmi" più volte: ad esempio una applicazione che contiene due istanze dello stesso tipo di utilizzatore, avrà due gruppi identici di allarmi montati che saranno differenziati usando i campi instance e offset...

pertanto, questa è una funzionalità molto interessante ma non implementata in questa versione light del Alarm Manager: c'è da aggiungere un po' di codice per farla funzionare e, prima o poi, scriverò una terza parte dell'articolo proprio per completare questo discorso, magari aggiungendo anche le funzioni mancanti citate sopra. Prima o poi, eh! Non siate impazienti...

Tornando al pezzo: vi ripropongo la ricetta finale della prima parte dell'articolo, che ora è, finalmente, possibile realizzare completamente, visto che abbiamo l'implementazione delle funzioni:

...ricetta finale: prendere il main e il tcpServer, aggiungere il modulo udpServer (ispirato al tcpServer), compilare bene, aggiungere un pizzico di sale ed eseguire. Vedrete che ogni 7 secondi si mostreranno gli allarmi attivi e storici, in maniera dinamica. Tra l'altro gli allarmi storici (che hanno, nel demo, un buffer di 8) quando raggiungono il limite cominciano a "circolare", cioè i più vecchi vengono cancellati per far posto ai nuovi. Provare per credere...

Ebbene si, sarà pure una versione light, con qualche funzione (e funzionalità) in meno, ma va liscia come l'olio! Compilate, gente, compilate...

Ciao, e al prossimo post!