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!
Lascia un commento