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.

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!

Nessun commento:

Posta un commento