Go by Example - IT: Gestione dello Stato con Goroutine

Nell’esempio precedente abbiamo usato il lock esplicito insieme alle mutex per sincronizzare l’accesso allo stato condiviso da più goroutine. Un’altra opzione è quella di utilizzare le funzionalità di sincronizzazione native di Go, sfruttando goroutine e channel per ottenere lo stesso risultato. Questo approcchio channel-based risulta più in linea con il principio di Go di avere la memoria condivisa tramite la comunicazione, ed ogni goroutine con la propria memoria privata.

package main
import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"
)

In questo esempio lo stato verrà gestito da una singola goroutine. Questo ci garantirà che i dati non verranno mai corrotti da accessi concorrenti. Per poter leggere ed aggiornare lo stato le altre goroutine dovranno inviare dei messaggi alla goroutine proprietaria che risponderà con un messaggio contenente il dato richiesto. Le struct readOp e writeOp incapsulano queste richieste e contengono il field resp per indicare la risposta della goroutine proprietaria.

type readOp struct {
    key  int
    resp chan int
}
type writeOp struct {
    key  int
    val  int
    resp chan bool
}
func main() {

Come prima terremo conto del numero di operazioni che andremo ad eseguire

    var ops int64 = 0

I channel reads e writes saranno utilizzati dalle altre goroutine per effettuare richieste di lettura e di scrittura dello stato.

    reads := make(chan *readOp)
    writes := make(chan *writeOp)

Questa sarà la goroutine che gestirà lo stato, rappresentato da una map come nell’esempio precedente, ma in questo caso visibile solo alla goroutine. Questa goroutine effettuerà delle select sui due channel e risponderà alle richieste che arrivano. La goroutine si occuperà di gestire la richiesta e restituirà un valore sul channel resp per indicare un successo (nel caso di una richiesta reads verrà restituito il valore richiesto).

    go func() {
        var state = make(map[int]int)
        for {
            select {
            case read := <-reads:
                read.resp <- state[read.key]
            case write := <-writes:
                state[write.key] = write.val
                write.resp <- true
            }
        }
    }()

Quì facciamo partire 100 goroutine che eseguiranno delle richieste di lettura dello stato verso la goroutine proprietaria tramite il channel reads. Ogni richiesta deve essere eseguita tramite la struct readOp, inviata sul channel reads e si attende il suo risultato sul channel resp.

    for r := 0; r < 100; r++ {
        go func() {
            for {
                read := &readOp{
                    key:  rand.Intn(5),
                    resp: make(chan int)}
                reads <- read
                <-read.resp
                atomic.AddInt64(&ops, 1)
            }
        }()
    }

Avviamo anche altre 10 goroutine che effettueranno delle scritture utilizzando un approccio simile.

    for w := 0; w < 10; w++ {
        go func() {
            for {
                write := &writeOp{
                    key:  rand.Intn(5),
                    val:  rand.Intn(100),
                    resp: make(chan bool)}
                writes <- write
                <-write.resp
                atomic.AddInt64(&ops, 1)
            }
        }()
    }

Facciamo eseguire le goroutine per un secondo.

    time.Sleep(time.Second)

Infine leggiamo il valore della variabile ops.

    opsFinal := atomic.LoadInt64(&ops)
    fmt.Println("ops:", opsFinal)
}

Eseguendo il nostro codice possiamo vedere che sono state eseguite circa 800.000 operazioni in un secondo.

$ go run stateful-goroutines.go
ops: 807434

Per questo caso particolare l’approccio a goroutine risulta migliore rispetto a quello basato su mutex. Questo approccio risulta vincente nel caso in cui si debbano gestire svariati channel oppure nei casi in cui la gestione di troppe mutex potrebbe risultare complesso. Il consiglio è quello di utilizzare l’approccio che risulta più naturale per ogni singolo caso e che mantiene alta la leggibilità del proprio codice.

Prossimo esempio: Ordinamento.