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!