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.

sabato 11 aprile 2020

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

Mezzacapa: Acqua, vento... e nebbia! Eh... nebbia, nebbia!
Totò: Ah, questo m'impressiona! Tutto, ma la nebbia.
Mezzacapa: A Milano, quando c'è la nebbia non si vede.
Totò: Perbacco... e chi la vede?
Mezzacapa: Cosa?
Totò: Questa nebbia, dico?
Mezzacapa: Nessuno.
Totò: Ma, dico, se i milanesi, a Milano, quando c'è la nebbia, non vedono, come si fa a vedere che c'è la nebbia a Milano?
Mezzacapa: No, ma per carità, ma quella non è una cosa che si può toccare.
Peppino: Ah, ecco.
Totò: Non si tocca... non si tocca.
Peppino: Ma io, a parte questa nebbia, io non la tocco per carità... Ma adesso se noi dobbiamo incontrare a nostro nipote, questa cantante, come li vediamo, dove li troviamo?
Totò: Già! Eh già, non ci avevo pensato.
Mezzacapa: È facile, la cantante, quella c'ha il nome sul manifesto.
Totò: Hai capito, a Milano quando c'è la nebbia, mettono i nomi sui manifesti. Dice, chi mi vuol trovare, io sto qua.
E finalmente siamo arrivati alla terza ed ultima parte della nostra avventura con il Watchdog (la prima e la seconda parte le trovate qui e qui). Anche in questo caso il surreale dialogo tra Totò, Peppino e Mezzacapa ci fornisce un indizio e una ispirazione: prendiamo il Watchdog in C e, a maggior ragione, quello in C++, mettiamo un po' di nebbia per omettere il superfluo et voilà! Il Watchdog ridotto all'osso è servito, scritto in un vero linguaggio ad alto livello, il Go
...Ah, questo m'impressiona! Tutto, ma il Watchdog in Go...
Dunque, vediamo: la presentazione di oggi è più "ristretta" di quelle precedenti, perché abbiamo un main() d'uso e un file di implementazione, e niente header, questo è il Go, gente! Cominciamo con l'implementazione, allora, vai col codice!

package main

import (
    "fmt"
    "sync"
    "time"
)

const MAX_WATCH = 32    // numero massimo di watch in uso

// Watch - tipo per watch nel watchdog
type Watch struct {
    ID     int          // identificatore del watch (numero)
    name   string       // identificatore del watch (stringa)
    active bool         // flag di attività (true=attivo)
}

// Watchdog - tipo per watchdog
type Watchdog struct {
    watchList  [MAX_WATCH]*Watch    // lista di watch
    watchMutex sync.Mutex           // mutex per operazioni add/set/check
}

// delete - elimina tutti i watch
func (wdg *Watchdog) delete() {

    // rilascia le risorse allocate
    for i := 0; i < MAX_WATCH; i++ {
        // check se il watch è disponibile
        if wdg.watchList[i] != nil {
            // cancella un watch
            wdg.delWatch(i)
        }
    }
}

// check - check di tutti i watch nella lista watch
func (wdg *Watchdog) check(
    waitSec time.Duration) {   // sleep del loop interno in secondi

    // loop infinito di check watch
    for {
        // lock di questo blocco per uso thread-safe
        wdg.watchMutex.Lock()

        // check di tutti i watch nella lista watch
        for i := 0; i < MAX_WATCH; i++ {
            // check solo dei watch in uso
            if wdg.watchList[i] != nil {
                // check del watch
                if wdg.watchList[i].active {
                    // watch attivo: reset flag active
                    wdg.watchList[i].active = false
                } else {
                    // watch inattivo: mostro l'errore
                    fmt.Printf("check: watch %d: %s goroutine inattiva\n",
                               wdg.watchList[i].ID, wdg.watchList[i].name)
                }
            }
        }

        // unlock del blocco
        wdg.watchMutex.Unlock()

        // sleep del loop
        time.Sleep(time.Second * waitSec)
    }
}

// addWatch - aggiunge un watch nella watch list
func (wdg *Watchdog) addWatch(
    name string) int {      // watch name

    // lock per uso thread-safe
    wdg.watchMutex.Lock()
    defer wdg.watchMutex.Unlock()

    // loop sulla watch list per trovare il primo watch disponibile
    for i := 0; i < MAX_WATCH; i++ {
        // check se il watch è disponibile
        if wdg.watchList[i] == nil {
            // aggiunge un watch in watch list
            wdg.watchList[i] = new(Watch)

            // set valori
            wdg.watchList[i].ID     = i
            wdg.watchList[i].name   = name
            wdg.watchList[i].active = false
            fmt.Printf("addWatch: watch aggiunto: ID=%d name=%s\n", i, name)

            // return ID
            return i
        }
    }

    // return errore
    fmt.Printf("addWatch: non ci sono più watch disponibili\n")
    return -1
}

// delWatch - cancella un watch nella watch list
func (wdg *Watchdog) delWatch(
    ID int) {               // watch ID

    // lock per uso thread-safe
    wdg.watchMutex.Lock()
    defer wdg.watchMutex.Unlock()

    // cancella un watch
    fmt.Printf("delWatch: cancella un watch: ID=%d name=%s\n",
               wdg.watchList[ID].ID, wdg.watchList[ID].name)
    wdg.watchList[ID] = nil
}

// setWatch - set a watch
func (wdg *Watchdog) setWatch(
    ID int) {               // watch ID

    // lock per uso thread-safe
    wdg.watchMutex.Lock()
    defer wdg.watchMutex.Unlock()

    // set a true del flag active
    if wdg.watchList[ID] != nil {
        wdg.watchList[ID].active = true
    }
}
Il Go permette una notevole libertà di stile di implementazione, e in questo caso ho scelto di scrivere un Watchdog che usa metodi invece di funzioni e, vista l'assenza di classi, il risultato è un po' una via di mezzo tra la versione in C e quella in C++, anche considerando che le due strutture usate sono quasi identiche a quelle scritte in C. I metodi realizzati sono molto (ma molto) simili a quelli delle altre versioni e, come avrete notato, con c'è un metodo di setup o di costruzione, visto che non c'era nulla da inizializzare. Non credo che ci sia più nulla da aggiungere sull'implementazione, sia perché è, al solito, ben commentata, sia perché è veramente molto simile a quelle viste nelle prime due parti dell'articolo.

E allora passiamo al punto più interessante, il main(). Vediamolo!
package main

import (
    "fmt"
    "time"
    "sync"
)

// main() - LocalController main function
func main() {

    // crea il watchdog
    watchdog := Watchdog{}

    // waitgroup di attesa terminazione goroutine
    var wg sync.WaitGroup
    wg.Add(2)

    // avvio goroutine A e B
    go myGoroutineA(&wg, &watchdog)
    go myGoroutineB(&wg, &watchdog)

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

    // attesa terminazione goroutine
    wg.Wait()
    fmt.Printf("main: goroutine terminate\n")
}

// goroutine A
func myGoroutineA(wg *sync.WaitGroup, watchdog *Watchdog) {

    // all'uscita decremento il waitgroup
    defer wg.Done()

    // aggiunge un watch per questa goroutine
    watchID := watchdog.addWatch("myGoroutineA")
    if watchID < 0 {
        // errore: non posso usare il watch
        fmt.Printf("myGoroutineA: non posso usare il watch: fermo la goroutine A")
        return
    }

    // loop della goroutine
    fmt.Printf("goroutine A partita\n")
    i := 0
    for {
        // la goroutine fa cose...

        // ...

        // TEST: ogni 5 secondi simulo un blocco della goroutine
        i++
        if i == 500 {
            fmt.Printf("goroutine A: sleep di 5 sec\n")
            i = 0
            time.Sleep(time.Second * 5)
        }

        // rinfresco il watch della goroutine
        watchdog.setWatch(watchID)

        // sleep della goroutine (10 ms)
        time.Sleep(time.Millisecond * 10)
    }

    // la goroutine esce
    fmt.Printf("goroutine A finita\n")
}

// goroutine B
func myGoroutineB(wg *sync.WaitGroup, watchdog *Watchdog) {

    // all'uscita decremento il waitgroup
    defer wg.Done()

    // aggiunge un watch per questa goroutine
    watchID := watchdog.addWatch("myGoroutineB")
    if watchID < 0 {
        // errore: non posso usare il watch
        fmt.Printf("myGoroutineB: non posso usare il watch: fermo la goroutine B")
        return
    }

    // loop della goroutine
    fmt.Printf("goroutine B partita\n")
    i := 0
    for {
        // la goroutine fa cose...

        // ...

        // TEST: ogni 15 secondi simulo un blocco della goroutine
        i++
        if i == 1500 {
            fmt.Printf("goroutine B: sleep di 5 sec\n")
            i = 0
            time.Sleep(time.Second * 5)
        }

        // rinfresco il watch della goroutine
        watchdog.setWatch(watchID)

        // sleep della goroutine (10 ms)
        time.Sleep(time.Millisecond * 10)
    }

    // la goroutine esce
    fmt.Printf("goroutine B finita\n")
}
Inutile soffermarsi sulle due goroutine che sono perfettamente equivalenti ai thread delle versioni C e C++, quindi andiamo direttamente alla funzione main() che è di una semplicità veramente sorprendente, ed è così compatta (e commentata) che dubito che ci sia qualcosa da spiegare. Praticamente l'unica parte "strana", per chi non è molto pratico del Go, potrebbe essere il sync.WaitGroup, che poi non è nient'altro che una semplicissima maniera di raggruppare le goroutine che si useranno per sorvegliarne la terminazione: una operazione equivalente alla join del C/C++ ma più semplificata, quindi. 

Riepiloghiamo: il main() della versione C era molto compatto e lineare, e usava la classica e un po' verbosa gestione degli errori del C. Il main() della versione C++ era apparentemente ancora più sintetico ma, come ben evidenziato nell'articolo precedente, la gestione delle eccezioni sugli oggetti creati nasconde molte insidie, quindi il codice finale reale era, in realtà, molto più complesso. Il main() della versione Go è, invece, veramente semplice e non c'è da aggiungere nient'altro rispetto a quello mostrato. Una vera sciccheria!
Considerazioni finali: per trasformare in codice reale di produzione gli esempi semplificati (C, C++ e Go) visti, non c'è, in realtà, molto lavoro aggiuntivo da eseguire, e posso dare qualche consiglio valido (più o meno) per tutti e tre i linguaggi:
  1. La funzione di check dovrebbe avere una condizione di uscita (il loop dovrebbe essere pseudo-infinito). Ad esempio si potrebbe decidere che quando tutti i thread sorvegliati sono bloccati o terminati (bene o per errore) la funzione dovrebbe uscire segnalando il problema e avviare la chiusura controllata del processo main.
  2. Anche i thread (o goroutine) da sorvegliare dovrebbero avere una condizione di uscita, per permettere la chiusura controllata del processo main. Potrebbe anche essere una buona idea eseguire il detach dei thread e controllare l'uscita solo attraverso il Watchdog.
  3. Bisognerebbe sofisticare adeguatamente (come già accennato nella parte 1 dell'articolo) la funzione di check in maniera di poter gestire in maniera adeguata thread veloci e thread lenti: dovrebbe essere compito del thread stesso comunicare la cadenza di sorveglianza nella fase di registrazione al Watchdog.
La nostra avventura col Watchdog in tre linguaggi è terminata. Credo di aver fornito abbastanza materiale per poter scrivere un codice di produzione in C, C++ e Go. Ognuno può evidentemente scegliere la versione più opportuna in base alle proprie preferenze, inclinazioni ed esigenze di progetto. Io propendo sempre (inutile nasconderlo) per scrivere nel mio amato C (che è un vero linguaggio 4WD, solidissimo e multiuso), ma ultimamente il Go mi intriga molto.

Sul C++ ho già fatto le mie considerazioni in altri articoli, preferisco non infierire... anzi si, infierisco e vi lascio con un estratto di una bella introduzione al linguaggio Go da parte di uno dei suoi tre autori, Rob Pike (gli altri due sono il mitico Ken Thompson e Robert Griesmer). L'articolo si chiama "Less is exponentially more", ed è, già nel titolo, una presentazione di intenti del linguaggio Go: consiglio a tutti di leggerlo per intero. La parte che ho estratto spiega, in maniera divertente, che uno degli impulsi alla creazione di Go è stato, paradossalmente, l'uscita del C++11... (…oops: mi dicono dalla regia che è possibile che alcuni colleghi informatici non conoscano Rob Pike o Ken Thompson… e vabbè, continuiamo così, facciamoci del male). Vai Rob!
"...Back around September 2007, I was doing some minor but central work on an 
enormous Google C++ program, one you've all interacted with, and my compilations 
were taking about 45 minutes on our huge distributed compile cluster. An 
announcement came around that there was going to be a talk presented by a couple 
of Google employees serving on the C++ standards committee. They were going to 
tell us what was coming in C++0x, as it was called at the time. (It's now known 
as C++11).

In the span of an hour at that talk we heard about something like 35 new features 
that were being planned. In fact there were many more, but only 35 were described 
in the talk. Some of the features were minor, of course, but the ones in the talk 
were at least significant enough to call out. Some were very subtle and hard to 
understand, like rvalue references, while others are especially C++-like, such as 
variadic templates, and some others are just crazy, like user-defined literals.

At this point I asked myself a question: Did the C++ committee really believe that
was wrong with C++ was that it didn't have enough features? Surely, in a variant 
of Ron Hardin's joke, it would be a greater achievement to simplify the language 
rather than to add to it. Of course, that's ridiculous, but keep the idea in mind..."

                                      da "Less is exponentially more", Rob Pike, 2012

Quindi ricordate: less is exponentially more! E ho detto tutto!

Ciao, e al prossimo post!

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!