Tot├▓, Peppino e il Watchdog – come scrivere un Watchdog in C, C++ e Go – pt.2

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!

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

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Follow Me