[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!
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:
- 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ì.
- 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; } ...
- 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!
https://italiancoders.it/thread-cancel-no-grazie-considerazioni-sul-perche-non-usare-la-pthread_cancel3-in-c-e-c-pt-2/