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ì 26 settembre 2022

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!

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

// 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 "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
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!