Totò, Peppino e il Watchdog
come scrivere un Watchdog in C, C++ e Go – pt.2
Totò: Noio, volevan, volevon, savuar, noio volevan savuar l'indiriss, ia? Vigile: Eh ma, bisogna che parliate l'italiano perché io non vi capisco. Totò: Parla italiano? Parla italiano! Peppino: Complimenti! Totò: Complimenti! Parla italiano! bravo! Vigile: Ma scusate, ma dove vi credevate di essere? Siamo a Milano qua. Totò: Appunto lo so. Dunque, noi vogliamo sapere, per andare, dove dobbiamo andare, per dove dobbiamo andare, sa è una semplice informazione. Vigile: Sentite... Totò e Peppino [in coro]: Signorsì? Vigile: Se volete andare al manicomio... Totò e Peppino [in coro]: Sìssignore? Vigile: Vi accompagno io. Ma varda un po' che roba, ma da dove venite voi? Dalla Val Brembana?
Anche questa famosissima scena di Totò, Peppino e la malafemmina si aggancia bene all’argomento di questa seconda parte dell’articolo sul Watchdog (immagino che la prima parte l’avete già letta e sapete anche recitarla a memoria, no?): abbiamo un problema di linguaggio nel dialogo con il vigile: le intenzioni sono buone, ma quando si costruiscono frasi troppo arzigogolate l’incomprensione è dietro l’angolo. Ecco, il nostro Watchdog nella sua versione C++ può indurre in qualche scelta dubbiosa, come vedremo tra poco.
…Noio, volevan, volevon, savuar, noio volevan savuar il Watchdog, ia?… |
Allora, veniamo al dunque: la presentazione di oggi è analoga a quella della versione C, quindi abbiamo un main() d’uso, un header e un file di implementazione, ma questa volta lasceremo per ultimo il main(), perché è (stranamente) la parte più problematica. Cominciamo allora con l’header, vai col codice!
#ifndef WATCHDOG_H #define WATCHDOG_H #include <mutex> using namespace std; #define MAX_WATCH 32 // numero massimo di watch in uso // definizione della struttura Watch struct Watch { int id; // identificatore del watch (numero) string name; // identificatore del watch (stringa) bool active; // flag di attività (true=attivo) }; // definizione della classe Watchdog class Watchdog { public: // metodi // // costruttore e distruttore Watchdog(); virtual ~Watchdog(); // metodo per check di tutti i watch nella lista watch void check(unsigned int wait_sec); // metodo per aggiungere un watch per un thread int addWatch(const string& name); // metodo per cancellare un watch void delWatch(int id); // metodo per set watch void setWatch(int id); private: // attributi // Watch *watch_list[MAX_WATCH]; // lista di watch mutex watch_mutex; // mutex per operazioni add/set/check }; #endif /* WATCHDOG_H */
Come sempre il codice è abbondantemente commentato è non c’è quasi bisogno di spiegarlo. L’header è molto simile a quella della versione C, quindi abbiamo una struttura Watch che descrive i punti di sorveglianza e una classe Watchdog che è, praticamente, identica alla struttura Watchdog della versione C, con l’aggiunta dei metodi pubblici della classe che ripetono esattamente le funzionalità delle funzioni globali della versione C. In definitiva è un header molto semplice e lineare. Ed ora possiamo passare all’implementazione, andiamo!
#include "watchdog.h" #include <cstdio> #include <unistd.h> using namespace std; // Watchdog - costruttore classe Watchdog Watchdog::Watchdog() { // reset pointers watchdog for (int i = 0; i < MAX_WATCH; i++) { // set pointer to 0 watch_list[i] = nullptr; } } // ~Watchdog - distruttore classe Watchdog Watchdog::~Watchdog() { // rilascia le risorse allocate for (int i = 0; i < MAX_WATCH; i++) { // check se il watch è disponibile if (watch_list[i] != nullptr) { // cancella un watch delWatch(i); } } } // check - check di tutti i watch nella lista watch void Watchdog::check( unsigned int wait_sec) // sleep del loop interno in secondi { // loop infinito di check watch for (;;) { // lock di questo blocco per uso thread-safe watch_mutex.lock(); // check di tutti i watch nella lista watch for (int i = 0; i < MAX_WATCH; i++) { // check solo dei watch in uso if (watch_list[i] != nullptr) { // check del watch if (watch_list[i]->active) { // watch attivo: reset flag active watch_list[i]->active = false; } else { // watch inattivo: mostro l'errore printf("%s: watch %d: %s thread inattivo\n", __func__, watch_list[i]->id, watch_list[i]->name.c_str()); } } } // unlock del blocco watch_mutex.unlock(); // sleep del loop sleep(wait_sec); } } // addWatch - aggiunge un watch nella watch list int Watchdog::addWatch( const string& name) // watch name { // lock per uso thread-safe lock_guard<mutex> mylock(watch_mutex); // loop sulla watch list per trovare il primo watch disponibile for (int i = 0; i < MAX_WATCH; i++) { // check se il watch è disponibile if (watch_list[i] == nullptr) { // aggiunge un watch in watch list watch_list[i] = new Watch; // set valori watch_list[i]->id = i; watch_list[i]->name = name; watch_list[i]->active = false; printf("%s: watch aggiunto: id=%d name=%s\n", __func__, i, name.c_str()); // return id return i; } } // return errore printf("%s: non ci sono più watch disponibili\n", __func__); return -1; } // delWatch - cancella un watch nella watch list void Watchdog::delWatch( int id) // watch id { // lock per uso thread-safe lock_guard<mutex> mylock(watch_mutex); // cancella un watch printf("%s: cancella un watch: id=%d name=%s\n", __func__, watch_list[id]->id, watch_list[id]->name.c_str()); delete watch_list[id]; watch_list[id] = nullptr; } // setWatch - set a watch void Watchdog::setWatch( int id) // watch id { // lock per uso thread-safe lock_guard<mutex> mylock(watch_mutex); // set a true del flag active if (watch_list[id] != nullptr) watch_list[id]->active = true; }
Ed anche qui possiamo notare che l’implementazione è speculare a quella della versione C, e i metodi sono quasi sovrapponibili alle funzioni corrispondenti (c’era da aspettarselo, no?). Qualche piccola differenza c’è nella gestione del mutex di sincronizzazione, visto che ho usato C++11, e quindi ho potuto usufruire della nuova interfaccia RAII dei mutex (C++11), e cioè std::lock_guard. Ed adesso siamo pronti per vedere il main(): forza che quasi ci siamo!
#include "watchdog.h" #include <cstdio> #include <thread> #include <stdlib.h> using namespace std; // prototipi locali void myThreadA(Watchdog* watchdog); void myThreadB(Watchdog* watchdog); // funzione main() int main(int argc, char* argv[]) { // crea il watchdog Watchdog watchdog; // avvio thread A e B thread th_A(myThreadA, &watchdog); thread th_B(myThreadB, &watchdog); // avvio check watchdog (contiene un loop infinito) watchdog.check(1); // sleep interna di 1 sec // attesa terminazione thread A if (th_A.joinable()) th_A.join(); // attesa terminazione thread B if (th_B.joinable()) th_B.join(); // exit printf("%s: thread terminati\n", argv[0]); return EXIT_SUCCESS; } // thread routine A void myThreadA(Watchdog* watchdog) { // aggiunge un watch per questo thread int watch_id; if ((watch_id = watchdog->addWatch("myThreadA")) < 0) { // errore: non posso usare il watch printf("%s: non posso usare il watch: fermo il thread myThreadA", __func__); return; } // loop del thread printf("thread A partito\n"); int i = 0; for (;;) { // il thread fa cose... // ... // TEST: ogni 5 secondi simulo un blocco del thread if (i++ == 500) { printf("thread A: sleep di 5 sec\n"); i = 0; this_thread::sleep_for(chrono::seconds(5)); } // rinfresco il watch del thread watchdog->setWatch(watch_id); // sleep del thread (10 ms) this_thread::sleep_for(chrono::milliseconds(10)); } // il thread esce printf("thread A finito\n"); } // thread routine B void myThreadB(Watchdog* watchdog) { // aggiunge un watch per questo thread int watch_id; if ((watch_id = watchdog->addWatch("myThreadB")) < 0) { // errore: non posso usare il watch printf("%s: non posso usare il watch: fermo il thread myThreadB", __func__); return; } // loop del thread printf("thread B partito\n"); int i = 0; for (;;) { // il thread fa cose... // ... // TEST: ogni 15 secondi simulo un blocco del thread if (i++ == 1500) { printf("thread B: sleep di 5 sec\n"); i = 0; this_thread::sleep_for(chrono::seconds(5)); } // rinfresco il watch del thread watchdog->setWatch(watch_id); // sleep del thread (10 ms) this_thread::sleep_for(chrono::milliseconds(10)); } // il thread esce printf("thread B finito\n"); }
Allora, vediamo un po’: questo main() segue (ovviamente) gli stessi passi di quello della versione C, e i due thread che si lanciano sono praticamente identici a quelli già visti. Entrando più in dettaglio si può notare che la funzione main() è leggermente più compatta di quella della versione del primo articolo, visto che mancano i test di successo dei vari passi (creazione del Watchdog e creazione/join dei thread) che in questo caso non sono utilizzabili. Compilando ed eseguendo questo programma si otterranno esattamente gli stessi risultati di quello della versione C (provare per credere!).
E allora dov’è la parte critica? Ecco, la funzione main() presentata è, diciamo, la versione “minimale” realizzabile, che è poi, ahimè, anche quella che si vede spesso nel comune codice multithreading scritto in C++11. Diciamo che funziona, ma è tutt’altro che a prova di bomba (dal punto di vista della sicurezza dell’esecuzione). È una versione minimale perché, perlomeno, prevede il join dei thread “joinabili”, e già questa è una cosa obbligatoria che molti si dimenticano di fare. Bisogna rendersi conto che attendere i thread “joinabili” con std::thread::join è una prassi da seguire sempre, anche quando apparentemente non serve. È un po’ come, guidando, si mette la freccia quando si svolta anche quando non ci sono altre macchine in circolazione: si fa sempre per non perdere l’abitudine a farlo automaticamente.
Quindi è buona abitudine usare std::thread::join anche quando il thread è stato “staccato” con std::thread::detach perchè il test std::thread::joinable ci assicura che tutto avvenga senza errori. E volete un esempio di un possibile problema reale? Se dimentichiamo di usare join (o detach) quando il main() finisce l’esecuzione vengono chiamati i distruttori dei thread “joinabili” che a loro volta chiamano std::terminate… e il risultato è un bel crash.
Ma questo non è niente, il vero problema è un altro: l’implementazione dei thread in C++11 non prevede una facile gestione degli errori, e questo a causa della terribile implementazione delle eccezioni integrata nel C++.
(…ho detto terribile? ma questo potrebbe attirarmi le ire di tutti i fan del C++… Allora lo ritiro, e lo faccio dire a uno molto più autorevole di me, lo faccio dire a Linus (e potrei farlo dire a molti altri, eh!). Quindi se non siete d’accordo prendetevela con lui. Ma state attenti, è un tipo molto irascibile…)
Quindi dobbiamo tenere conto che un thread potrebbe uscire per un errore e, in questo caso, propagare l’errore al main() non è semplicissimo. E allora il codice qui sopra, che sembrava compatto, bisognerebbe trasformarlo un po’… Un metodo relativamente semplice di tracciare le eccezioni potrebbe essere questo (nota: è quasi pseudo-codice, non avevo voglia di provarlo):
// pointer globale per le eccezioni static exception_ptr globalExceptionPtr = nullptr; // funzione main() int main() { // avvio thread thread th(myThread); // attesa terminazione thread if (th.joinable()) th.join(); // gestione eccezioni if (globalExceptionPtr) { try { // tutto ok? rethrow_exception(globalExceptionPtr); } catch (const exception &ex) { // eccezione intercettata cout << "Thread uscito con eccezione: " << ex.what() << "\n"; } } // exit return 0; } // thread routine void myThread() { try { // il thread fa cose... // ... // sleep del thread (10 ms) this_thread::sleep_for(chrono::milliseconds(10)); } catch (...) { // set del exception pointer globale nel case di una eccezione globalExceptionPtr = current_exception(); } }
Evidentemente una volta applicato questo stile al main() del nostro Watchdog tutta la compattezza della versione C++ va a ramengo… E sono stato generoso, perché si potrebbe complicare ulteriormente il discorso usando un approccio del seguente tipo (nota: è quasi pseudo-codice, non avevo voglia di provarlo):
// funzione main() void main() { promise<int> promise; // la promessa del thread (?) future<int> future = promise.get_future(); // il futuro del thread (?) // avvio thread thread thread(&threadMethod, ref(promise)); // test delle eccezioni while (future.valid()) { try { // tutto ok? int result = future.get(); } catch(const exception& ex) { // eccezione intercettata cout << "Thread uscito con eccezione: " << ex.what() << "\n"; } } // attesa terminazione thread if (th.joinable()) th.join(); } // thread routine void myThread(promise<int>& promise) { try { // il thread fa cose... // ... // sleep del thread (10 ms) this_thread::sleep_for(chrono::milliseconds(10)); } catch(...) { // intercetto l'eccezione promise.set_exception(current_exception()); } }
Considerazione finale: se per trasformare il C++ in un linguaggio ad alto livello (ma è veramente, ma veramente, necessario?) il comité ISO ha bisogno di aggiungere oggetti con nomi esotici e funzionamento misterioso come std::future e std::promise (e tantissimi altri oggetti che vi risparmio) io rimango veramente perplesso (eufemismo). Ma ricordate: tutto questo è molto soggettivo! Ovvero: io rimango perplesso di fronte a cose come questa, mentre sono sicuro che ad altri potrebbe venire un orgasmo. Il mondo è bello perché è vario…
E con questa seconda parte è tutto. Nella terza e ultima parte vi presenterò la versione Go del Watchdog. E vi preannuncio che il main() questa volta sarà veramente compatto, senza promesse e futuri, come si adddice a un vero linguaggio ad alto livello.
Ciao, e al prossimo post!