Gestione Dello Stato Con Goroutine

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.