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.

venerdì 20 marzo 2020

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!

mercoledì 19 febbraio 2020

Totò, Peppino e il Watchdog
come scrivere un Watchdog in C, C++ e Go - pt.1

Peppino: Che.
Totò: Che! Scusate se sono poche.
Peppino: Che…
Totò: Che, scusate se sono poche, ma settecentomila lire, punto e virgola, noi, noi ci fanno specie che quest’anno, una parola, quest’anno c’è stato una grande moria delle vacche, come voi ben sapete! Punto! Due punti!! Ma si, fai vedere che abbondiamo. Abbondandis in abbondandum. Questa moneta servono, questa moneta servono, questa moneta servono che voi vi consolate. Scrivi presto!
Peppino: Con insalata.
Totò: Che voi vi consolate!
Peppino: Ah! Avevo capito con l’insalata.
L'argomento della mitica e famosissima lettera di Totò e Peppino era, per loro, molto importante, era una questione familiare di un certo rilievo. Ed anche noi, con questo articolo, tratteremo un argomento di un certo peso, anche se non familiare: sarà un argomento industriale. Ebbene si, oggi parleremo di un oggetto per molti misterioso, il Watchdog.
...ma si, fai vedere che abbondiamo. Watchdog in abbondandum...
Una applicazione industriale che si rispetti (specialmente se embedded) include sempre un Watchdog a più livelli. vediamoli:
  • primo livello: Watchdog Hardware. Questo non può mancare: in presenza di malfunzionamenti che provocano il blocco o lo stallo della applicazione si può forzare il reset e riavvio del sistema.
  • secondo livello: Watchdog Software dei processi. Si monitorizzano i vari processi che compongono l'applicazione (su Linux, ad esempio, si può fare con Monit) e si effettuano eventuali attività di recupero in caso di necessità (restart di quelli "morti", ad esempio).
  • terzo livello: Watchdog Software dei thread. Se l'applicazione da monitorare è multithread si controlla che tutti i thread funzionino correttamente, ossia si controlla che nessuno di essi rimanga bloccato in attesa di qualche evento che non arriva mai, oppure che nessun thread lavori a velocità molto più bassa di quella prevista e/o ammessa, ecc. e, in caso di problemi, si agisce opportunamente (come minimo alzando degli allarmi per segnalare la situazione).
Perché ho affermato che il Watchdog è un oggetto per molti misterioso? Perché, anche se quasi tutte le applicazioni industriali usano quello di primo livello, alcune omettono quello di secondo e moltissime (ahimè) non usano quello di terzo livello (e questo, secondo me, è molto grave). Ma niente paura! vedremo in questo post ben tre semplici implementazioni di un Watchdog di terzo livello: in C, C++ e Go, così dopo avrete solo l'imbarazzo della scelta su quale usare.

Come funziona un Watchdog di terzo livello? Il modus operandi è abbastanza semplice, e include il rispetto di poche direttive di base:
  • i thread da monitorare devono avere una struttura "classica", e cioè quella di una funzione "che fa cose" in loop infinito con un opportuno intervallo di sleep tra un ciclo e l'altro.
  • i thread da monitorare devono registrarsi al Watchdog prima di avviare il loop infinito.
  • nessuna delle cose che il thread fa nel loop deve essere bloccante: ad esempio se si legge da un socket questo deve essere stato aperto in modo nonblocking.
  • ad ogni giro del loop (appena prima della sleep) si deve aggiornare una variabile di monitoring che verrà letta dal Watchdog vero e proprio.
Viste le direttive di base si può già dedurre che cosa è il Watchdog: è una funzione che esegue un loop infinito in cui testa le variabili di monitoring dei thread che si sono registrati. Anche questo loop avrà una sleep che, al contrario di quella dei thread che, tipicamente, è piccola (dell'ordine dei millisecondi) sarà grande (dell'ordine dei secondi), perché normalmente è inutile sorvegliare i thread con frequenze altissime (ma ci sono, ovviamente, delle eccezioni). Se il Watchdog si accorge che un thread non risponde (ossia: non rinfresca la variabile di stato), prenderà gli opportuni provvedimenti che dipendono dalla natura dell'applicazione (alzare allarmi, effettuare una procedura di recovery, ecc.).

Scrivere il codice di un Watchdog è relativamente semplice ma, ovviamente, si può complicare a piacere. La versione che vi proporrò, ad esempio, controlla tutti i thread con una cadenza fissa, impostata da un valore in secondi che si passa al Watchdog. Quindi un primo livello di complicazione potrebbe essere quello di avere intervalli di sorveglianza indipendenti e impostati dal thread nella fase di registrazione: in questa maniera un unico Watchdog potrebbe sorvegliare velocemente alcuni thread e lentamente altri. E così via, si possono aggiungere e/o perfezionare prestazioni, ma il modello base che vedremo è, sicuramente, una buona base di lavoro per usi reali.

Cominceremo con la versione C (qualcuno lo dubitava?) e, prima di mostrare il Watchdog, vedremo un esempio d'uso con test incorporato: ho scritto un main() che avvia due thread passandogli un pointer al Watchdog. I thread non fanno nulla ma, ogni tanto, smettono di dare segni di vita, permettendoci di osservare realmente cosa fa il nostro Watchdog in questi casi. Vai col codice!
#include "watchdog.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

// prototipi locali
void* myThreadA(void *arg);
void* myThreadB(void *arg);

// funzione main()
int main(int argc, char* argv[])
{
    int error;

    // init del watchdog
    Watchdog watchdog;
    if (setWatchdog(&watchdog) != 0) {
        printf("%s: non posso usare il watchdog\n", argv[0]);
        return EXIT_FAILURE;
    }

    // avvio thread A
    pthread_t tid_A;
    if ((error = pthread_create(&tid_A, NULL, &myThreadA, (void *)&watchdog)) != 0) {
        printf("%s: non posso creare il thread A (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // avvio thread B
    pthread_t tid_B;
    if ((error = pthread_create(&tid_B, NULL, &myThreadB, (void *)&watchdog)) != 0) {
        printf("%s: non posso creare il thread A (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // avvio check watchdog (contiene un loop infinito)
    chkWatchdog(&watchdog, 1);  // sleep interna di 1 sec

    // attesa terminazione thread A
    if ((error = pthread_join(tid_A, NULL)) != 0) {
        printf("%s: non posso unire il thread A (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // attesa terminazione thread B
    if ((error = pthread_join(tid_B, NULL)) != 0) {
        printf("%s: non posso unire il thread B (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // esce con Ok
    printf("%s: thread terminati\n", argv[0]);
    return EXIT_SUCCESS;
}

// thread routine A
void* myThreadA(void *arg)
{
    // ottengo i dati del thread con un cast (tdata*) di (void*) arg
    Watchdog *watchdog = (Watchdog *)arg;

    // aggiunge un watch per questo thread
    int watch_id;
    if ((watch_id = addWatch(watchdog, "myThreadA")) < 0) {
        // errore: non posso usare il watch
        printf("%s: non posso usare il watch: fermo il thread myThreadA", __func__);
        return NULL;
    }

    // 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;
            sleep(5);
        }

        // rinfresco il watch del thread
        setWatch(watchdog, watch_id);

        // sleep del thread (10 ms)
        usleep(10000);
    }

    // il thread esce
    printf("thread A finito\n");
    return NULL;
}

// thread routine B
void* myThreadB(void *arg)
{
    // ottengo i dati del thread con un cast (tdata*) di (void*) arg
    Watchdog *watchdog = (Watchdog *)arg;

    // aggiunge un watch per questo thread
    int watch_id;
    if ((watch_id = addWatch(watchdog, "myThreadB")) < 0) {
        // errore: non posso usare il watch
        printf("%s: non posso usare il watch: fermo il thread myThreadA", __func__);
        return NULL;
    }

    // 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;
            sleep(5);
        }

        // rinfresco il watch del thread
        setWatch(watchdog, watch_id);

        // sleep del thread (10 ms)
        usleep(10000);
    }

    // il thread esce
    printf("thread B finito\n");
    return NULL;
}
È abbastanza semplice, no? Grazie ai commenti credo che sia sufficientemente auto-esplicativo, e non credo che ci sia molto da aggiungere: il main() inizializza il Watchdog, avvia i due thread e avvia la funzione chkWatchdog() che è il cuore del nostro sistema. I due thread si registrano ed entrano in un loop infinito che non fa nulla, a parte (come detto sopra) simulare un blocco ogni tanto per testare il corretto funzionamento del Watchdog.
E adesso vediamo l'header, watchdog.h:
#ifndef WATCHDOG_H
#define WATCHDOG_H

#include <pthread.h>
#include <stdbool.h>

#define MAX_WATCH   32  // numero massimo di watch in uso

// typedef del tipo Watch
typedef struct {
    int  id;            // identificatore del watch (numero)
    char name[16];      // identificatore del watch (stringa)
    bool active;        // flag di attività (true=attivo)
} Watch;

// typedef del tipo Watchdog
typedef struct {
    Watch           *watch_list[MAX_WATCH]; // lista di watch
    pthread_mutex_t watch_mutex;            // mutex per operazioni add/set/check
} Watchdog;

// prototipi globali
int  setWatchdog(Watchdog *watchdog);
void delWatchdog(Watchdog *watchdog);
void chkWatchdog(Watchdog *watchdog, unsigned int wait_sec);
int  addWatch(Watchdog *watchdog, char *name);
void delWatch(Watchdog *watchdog, int id);
void setWatch(Watchdog *watchdog, int id);

#endif /* WATCHDOG_H */
Come si nota ho definito due tipi, uno che descrive un punto di sorveglianza elementare (il tipo Watch) e uno che descrive il Watchdog vero e proprio (il tipo Watchdog) che è, alla fin fine, solo una lista di watch protetta da un mutex (ebbene si, stiamo parlando di multithreading, quindi un mutex ci voleva proprio).

Ed ora siamo, finalmente, pronti ad esaminare il codice contenuto in watchdog.c:
#include "watchdog.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

// setWatchdog - set iniziale del Watchdog
int setWatchdog(
    Watchdog *watchdog)     // watchdog pointer
{
    // init mutex
    int error;
    if ((error = pthread_mutex_init(&watchdog->watch_mutex, NULL)) != 0) {
        // errore fatale: fermo la inizializzazione
        printf("%s: non posso creare il mutex (%s)\n", __func__,  strerror(error));
        return -1;
    }

    // reset pointers watchdog
    for (int i = 0; i < MAX_WATCH; i++) {
        // set pointer to NULL
        watchdog->watch_list[i] = NULL;
    }

    // set watchdog Ok
    return 0;
}

// delWatchdog - elimina tutti i watch
void delWatchdog(
    Watchdog *watchdog)     // watchdog pointer
{
    // rilascia le risorse allocate
    for (int i = 0; i < MAX_WATCH; i++) {
        // check se il watch è disponibile
        if (watchdog->watch_list[i] != NULL) {
            // cancella un watch
            delWatch(watchdog, i);
        }
    }
}

// chkWatchdog - check di tutti i watch nella lista watch
void chkWatchdog(
    Watchdog     *watchdog, // watchdog pointer
    unsigned int wait_sec)  // sleep del loop interno in secondi
{
    // loop infinito di check watch
    for (;;) {
        // lock della funzione per uso thread-safe
        pthread_mutex_lock(&watchdog->watch_mutex);

        // check di tutti i watch nella lista watch
        for (int i = 0; i < MAX_WATCH; i++) {
            // check solo dei watch in uso
            if (watchdog->watch_list[i] != NULL) {
                // check del watch
                if (watchdog->watch_list[i]->active) {
                    // watch attivo: reset flag active
                    watchdog->watch_list[i]->active = false;
                }
                else {
                    // watch inattivo: mostro l'errore
                    printf("%s: watch %d: %s thread inattivo\n",
                           __func__, watchdog->watch_list[i]->id, watchdog->watch_list[i]->name);
                }
            }
        }

        // unlock della funzione
        pthread_mutex_unlock(&watchdog->watch_mutex);

        // sleep del loop
        sleep(wait_sec);
    }
}

// addWatch - aggiunge un watch nella watch list
int addWatch(
    Watchdog *watchdog,     // watchdog pointer
    char     *name)         // watch name
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&watchdog->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 (watchdog->watch_list[i] == NULL) {
            // aggiunge un watch in watch list
            watchdog->watch_list[i] = malloc(sizeof(Watch));

            // set valori
            watchdog->watch_list[i]->id = i;
            snprintf(watchdog->watch_list[i]->name,
                     sizeof(watchdog->watch_list[i]->name), "%s", name);
            watchdog->watch_list[i]->active = false;
            printf("%s: watch aggiunto: id=%d name=%s\n", __func__, i, name);

            // unlock della funzione
            pthread_mutex_unlock(&watchdog->watch_mutex);

            // return id
            return i;
        }
    }

    // watch disponibili finiti: mostro errore
    printf("%s: non ci sono più watch disponibili\n", __func__);

    // unlock della funzione
    pthread_mutex_unlock(&watchdog->watch_mutex);

    // return errore
    return -1;
}

// delWatch - cancella un watch nella watch list
void delWatch(
    Watchdog *watchdog,     // watchdog pointer
    int      id)            // watch id
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&watchdog->watch_mutex);

    // cancella un watch
    printf("%s: cancella un watch: id=%d name=%s\n",
           __func__, watchdog->watch_list[id]->id, watchdog->watch_list[id]->name);
    free(watchdog->watch_list[id]);
    watchdog->watch_list[id] = NULL;

    // unlock della funzione
    pthread_mutex_unlock(&watchdog->watch_mutex);
}

// setWatch - set di un watch
void setWatch(
    Watchdog *watchdog,     // watchdog pointer
    int      id)            // watch id
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&watchdog->watch_mutex);

    // set a true del flag active
    if (watchdog->watch_list[id] != NULL)
        watchdog->watch_list[id]->active = true;

    // unlock della funzione
    pthread_mutex_unlock(&watchdog->watch_mutex);
}
Anche questo è molto commentato ed è auto-esplicativo. Si può solo aggiungere che la variabile di monitoring è una booleana che il thread associato mette a true (dicendo "sono vivo"): la funzione chkWatchdog() la legge e la rimette a false (dicendo "al prossimo giro devi dimostrami che sei vivo"). Tutte le operazioni che coinvolgono la variabile di stato sono protette dal mutex (ovviamente) e così anche quelle di creazione/rimozione dei watch. Nel caso che un thread risulti inattivo dopo l'intervallo di check, il problema viene segnalato (solo con una printf() in questo semplice esempio).
Che ne dite? direi che su questa base si può costruire un vero Watchdog "industriale", ad esempio abbinandolo a un gestore di allarmi (segnatevelo: in futuro parleremo anche di questo). E credo che per oggi può bastare: nella seconda parte esamineremo le altre versioni che ho scritto (in C++ e Go), e vi anticipo che, difficilmente, riuscirò a evitare qualche piccola polemica parlando della versione C++ (ma non si sa mai, dipenderà dall'umore del momento...).

Ciao, e al prossimo post!