Contatori Atomici
Se più goroutine tentano di accedere in scrittura alla stessa variabile, si ha una condizione di corsa.
E' necessario che tale variabile sia gestita atomicamente, cioè con accesso garantito singolo.
Nell'esempio è un contatore che viene incrementato da molte goroutines in esecuzione simultanea.
Un certo numero di funzioni del packagesync/atomic
permettono le operazioni atomiche, tra cui:
LoadUint64
- letturaAddUint64
- incremento
Tali funzioni operano sulla variabile per riferimento.
Esempio
(360atomic-counters.go):
package main
import "fmt"
import "time"
import "sync/atomic"
import "runtime"
func main() {
// Un unico contatore
var ops uint64 = 0
// 50 goroutine che simultaneamente incrementano il
// singolo contatore
for i := 0; i < 50; i++ {
go func() {
for {
// Incrementa il contatore
// L'operazione è atomica cioè
// garantisce l'accesso esclusivo
atomic.AddUint64(&ops, 1)
// Passa il controllo allo schedulatore
// per permettere il run delle
// altre goroutine
runtime.Gosched()
}
}()
}
// Attende un secondo per dare tempo
// a tutte le goroutine di partire
time.Sleep(time.Second)
// Non è possibile un semplice Println del contatore
// Occorre copiarlo atomicamente in una variabile
opsNow := atomic.LoadUint64(&ops)
fmt.Println("ops:", opsNow)
// Attene ancora un secondo e stampa di nuovo
time.Sleep(time.Second)
// Notare che la variabile ha già un tipo
// quindi si usa = non :=
opsNow = atomic.LoadUint64(&ops)
fmt.Println("ops:", opsNow)
}
E' opportuno che subito dopo le operazioni atomiche il controllo passi allo schedulatore, con la funzione Gosched
del package runtime
, per permettere ad altre goroutines di essere attivate.
Se tale funzione non è invocata in questo esempio il programma rimane appeso.
Senza contatore atomico
E' possibile non usare il contatore atomico come nel seguente programma:
(361-non-atomic-counters.go):
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ops uint64 = 0
for i := 0; i < 1; i++ {
go func() {
for {
ops++
runtime.Gosched()
}
}()
}
time.Sleep(time.Second)
opsNow := ops
fmt.Println("ops:", opsNow)
time.Sleep(time.Second)
opsNow = ops
fmt.Println("ops:", opsNow)
}
Il programma sembra funzionare correttamente, ma in realtà avvengono dei problemi sottostanti non segnalati.
Provare a lanciarlo con:
go run -race 36a-non-atomic-counters.go
Viene segnalata la condizione di corsa. Con -race
viene caricato il modulo di gestione appropriato.
Notare che anche se riduciamo il numero di goroutines create a 1 sola, viene segnalata la condizione di corsa.
Attenzione
Ogni volta che una goroutine accede ad una variabile non locale in modo diretto, cioè non tramite un channel, c'è la possibilità di condizioni di corsa.