Thread Cancel? No, grazie! – considerazioni sul perchè non usare la pthread_cancel(3) in C e C++ – pt.2

T

Thread Cancel? No, grazie!
considerazioni sul perché non usare la pthread_cancel(3) in C e C++ – pt.2

[il gruppo, alla ricerca di una via d'uscita, si accorge di non essere sulla Terra guardando il cielo]
Royce: Abbiamo bisogno di un nuovo piano...

Come promesso nella prima parte dell’articolo è venuto il momento di dare qualche consiglio su come fermare un thread senza usare la pthread_cancel(3). Là il film collegato era il mitico Predator, e in questo, per rimanere in tema, ho scelto il buon Predators che, pur non essendo all’altezza del primo della saga, ne è un buon seguito (al contrario di Predator 2 su cui è meglio sorvolare). In Predators i protagonisti sono alle prese con una missione quasi impossibile (e mi scuso per lo spoiler: tornare sulla terra), una missione complicata come fermare efficacemente un thread usando la pthread_cancel(3)… Ma niente paura, qui vedremo come si può fare!

Thread Cancel
…ve l’avevo detto che andava a finire così usando la pthread_cancel(3)…

E allora, veniamo al dunque: immagino che tutti conosciate il principio del Rasodio di Occam, che più o meno dice:

E’ inutile fare con più ciò che può essere fatto con meno.

Ebbene si, applicando questo fantastico principio al nostro problema possiamo dire: “perché per fermare un thread devo usare la (demenziale) combinazione di pthread_setcancelstate(3) + pthread_setcanceltype(3) + pthread_key_create(3) + pthread_cleanup_push(3) + pthread_cleanup_pop(3) + pthread_getspecific(3) + pthread_setspecific(3) + pthread_cancel(3) + pthread_join(3) quando posso lasciare a lui l’incarico di fermarsi bene?”.  Uhm, detto così sembra facile… e in effetti lo è! Il trucco consiste nel progettare adeguatamente la funzione eseguita dal thread in maniera che abbia dei punti di uscita “puliti”, e senza usare nessuna funzione accessoria della libpthread, che, come abbiamo visto, sono (per questa particolare situazione) molte, macchinose e anche complicate da usare.

Ok, bando alle ciance, facciamo parlare il codice: ecco un esempio molto semplice di uscita controllata che ho ottenuto applicando pochi piccoli cambi al codice di threaderrcancel.c  che, nello scorso articolo, chiudeva male il thread. Vai col codice!

// threadstop.c -esempio di stop thread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// prototipi globali
static void* myThread(void *arg);
static void  mySleep(unsigned int milliseconds);

// main() - funzione main()
int main(int argc, char* argv[])
{
    // creo il thread con un argomento "stop"
    int stop = 0;
    pthread_t tid;
    pthread_create(&tid, NULL, &myThread, &stop);

    // aspetto 2 secondi e poi stop del thread
    mySleep(2000);
    stop = 1;

    // join del thread
    pthread_join(tid, NULL);

    // esco
    printf("%s: esco\n", argv[0]);
    return 0;
}

// myThread() - thread routine
void* myThread(void *arg)
{
    // recast dell'argomento di stop thread
    int *stop = (int *)arg;

    // loop del thread
    printf("thread partito\n");
    while (*stop == 0) {
        // il thread fa cose...
        // ...

        // malloc sul buffer
        char *p = (char *)malloc(100);

        // simulo un ritardo perchè il thread fa altre cose...
        mySleep(2);

        // free del buffer
        free(p);

        // sleep del thread (10 ms)
        mySleep(10);
    }

    // il thread esce
    printf("thread finito\n");
    pthread_exit(NULL);
}

// mySleep() - wrapper per nanosleep()
static void mySleep(unsigned int milliseconds)
{
    struct timespec ts;
    ts.tv_sec = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000;
    nanosleep(&ts, NULL);
}

Non so se avete notato: è praticamente lo stesso codice dell’altro con 5 (cinque!) linee modificate. Il semplicissimo trucco consiste nel passare al thread un flag (con il fantasiosissimo nome “stop”) e usarlo adeguatamente nella funzione del thread. Nel nostro esempio la funzione esegue un loop pseudo-infinito, che ha come condizione di chiusura proprio il valore del flag di stop. Quando il main()  scrive 1 nel flag (invece di chiamare la pthread_cancel(3)) il thread finirà di eseguire il ciclo loop e, invece di eseguire un nuovo ciclo, uscirà chiamando pthread_exit(3). Ok, questo è un esempio molto semplificato, ma con qualche accortezza si può applicare a qualsiasi modello di thread routine. Anzi, possiamo fare un piccolo specchietto dei tre casi più classici di funzione di thread:

  1. Funzione con loop pseudo-infinito semplice: il thread esegue poche operazioni self-cleaning in loop. È quello dell’esempio appena mostrato, e con il test del flag di stop nel while siamo a posto così.
  2. Funzione con loop pseudo-infinito complesso: il thread esegue molte operazioni nel loop. Oltre al test del flag di stop nel while possiamo aggiungere delle istruzioni di uscita intermedie di questo tipo:
    ...
    if (stop == 1) {
        // pulisco memoria, lock, ecc.
        ...
    
        // esco dal loop (ma potrei anche uscire direttamente con pthread_exit(3))
        break;
    }
    ...
  3. Funzione senza loop pseudo-infinito: il thread esegue alcune attività sequenziali. Possiamo aggiungere dopo ogni attività delle istruzioni di uscita intermedie di questo tipo:
    ...
    if (stop == 1) {
        // pulisco memoria, lock, ecc.
        ...
    
        // esco
        pthread_exit(NULL);
    }
    ...
    

Qualcuno dirà: “ma le attività di pulizia prima della chiusura sono le stesse viste l’altra volta con pthread_cleanup_push(3), ecc., quindi è la stessa cosa…”. De gustibus: se sembra la stessa cosa ognuno è libero di continuare a impazzire usando la pthread_cancel(3) e le altre 1000 funzioni accessorie. A me sembra molto più semplice usare il flag di stop come appena mostrato… ma non voglio certo privare nessuno del piacere di scrivere codice più complicato del necessario (ah ah ah).

E chiudo con un (spero utile) consiglio: è meglio non confidare troppo nella combinazione pthread_create(3) + pthread_join(3): se qualcosa va male nello stop del thread (chessoio: una operazione di I/O bloccata nel thread) la pthread_join(3) blocca il programma infinitamente. Qualche furbone ovvia al problema eseguendo pthread_detach(3) sul thread appena creato, ma questa non è una scelta raccomandabile, perché si perde completamente il controllo dei thread creati e anche la possibilità di un vero stop controllato. Ci sono, invece, due opzioni molto più interessanti: la prima è usare una vera join con timeout, la pthread_timed_join_np(3), che funziona così (vi mostro solo il main(), nel resto del codice non cambia nulla):

// main() - funzione main()
int main(int argc, char* argv[])
{
    // creo il thread con un argomento "stop"
    int stop = 0;
    pthread_t tid;
    pthread_create(&tid, NULL, &myThread, &stop);

    // aspetto 2 secondi e poi stop del thread
    mySleep(2000);
    stop = 1;

    // join del thread con timeout di 1 sec
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);    // prendo il tempo attuale
    timeout.tv_sec += 1;    // il timeout è il tempo attuale più 1 sec
    int retval = pthread_timedjoin_np(tid, NULL, &timeout);

    // esco
    printf("%s: esco (%s)\n", argv[0],
           retval == ETIMEDOUT ? "con timeout" : "con Ok");
    return 0;
}

Oppure, se non è disponibile una la join con timeout (e, ahimè, per le interfacce threads di C11 e std::thread di C++11 è così), si può fare questo (e qui ci sono più cambi, quindi vi mostro quasi tutto il codice, omettendo le parti invariate):

// struttura per stop controllato
typedef struct _stops {
    int stop;
    int stopped;
} Stops;

// main() - funzione main()
int main(int argc, char* argv[])
{
    // creo il thread con un argomento "stops"
    Stops stops;
    stops.stop    = 0;
    stops.stopped = 0;
    pthread_t tid;
    pthread_create(&tid, NULL, &myThread, &stops);

    // aspetto 2 secondi e poi stop del thread
    mySleep(2000);
    stops.stop = 1;

    // detach del thread (in chiusura si può fare!)
    pthread_detach(tid);

    // simula una join con timeout
    int sleep_sum = 0;
    while (stops.stopped == 0) {
        // sleep di attesa
        mySleep(100);
        if ((sleep_sum += 100) > 1000) {
            // timeout scaduto: forzo l'uscita
            break;
        }
    }

    // esco
    printf("%s: esco (%s)\n", argv[0],
           sleep_sum > 1000 ? "con timeout" : "con Ok");
    return 0;
}

// myThread() - thread routine
void* myThread(void *arg)
{
    // recast degll'argomento
    Stops *stops = (Stops *)arg;

    // loop del thread
    printf("thread partito\n");
    while (stops->stop == 0) {
        // il thread fa cose...
        // ...

        // malloc sul buffer e pop cleanup di emergenza
        char *p = (char *)malloc(100);

        // simulo un ritardo perchè il thread fa altre cose...
        mySleep(2);

        // free del buffer
        free(p);

        // sleep del thread (10 ms)
        mySleep(10);
    }

    // segnala lo stop avvenuto
    stops->stopped = 1;

    // il thread esce
    printf("thread finito\n");
    pthread_exit(NULL);
}

Questo ultimo caso è leggermente più complesso (ripeto: leggermente) perché bisogna passare un parametro complesso al thread (per comandare lo stop e controllare se è avvenuto) e bisogna simulare la pthread_timed_join_np(3) usando la pthread_detach(3) seguita da un loop di attesa del flag stopped. Come detto sopra non bisognerebbe quasi mai usare pthread_detach(3), ma in questo caso si può fare un eccezione visto che la chiamiamo quando il thread sta già chiudendo la sua attività.

Per oggi può bastare, nel prossimo articolo cambieremo argomento, non voglio più sentir parlare di pthread_cancel(3) per un po’ di tempo… anzi, fosse per me la eliminerei, ma questo credo che si era capito (ah ah ah).

Ciao, e al prossimo post!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti