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

  ICT, Rassegna Stampa
image_pdfimage_print
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!

https://italiancoders.it/toto-peppino-e-il-watchdog-come-scrivere-un-watchdog-in-c-c-e-go-pt-3/