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

