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 - lettura
  • AddUint64 - 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.