Goroutines Stateful
Il problema di garantire l'integrità dei dati si può anche risolvere incaricando una sola goroutine della responsabilità di accedervi.
Tutte le altre goroutine che vogliono leggere e/o scrivere dati lo chiedono alla goroutine preposta tramite operazioni su channels.
Esempio
(380stateful-goroutines.go):
// I channels sono già primitive di sincronizzazione
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// Lo stato sarà possesso esclusivo di una goroutine
// Le altre goroutine chiedono letture e scritture
// tramite dei chan
// Struct per la lettura
type readOp struct {
key int // chiave
resp chan int // responso
}
// Struct per la scrittura
type writeOp struct {
key int // chiave
val int // valore
resp chan bool // conferma
}
func main() {
// Contatore delle operazioni compiute\
var ops int64 = 0
// Channels usati dalle altre goroutines
// Channel di lettura
// Notare che sono puntatori alle struct
// così sono modificabili gli originali
reads := make(chan *readOp)
// Channel di scrittura
writes := make(chan *writeOp)
// Goroutine che controlla lo stato
go func() {
// Mappa che rappresenta lo stato
var state = make(map[int]int)
for {
// Scandisce i canali di comunicazione
select {
case read := <-reads:
// Fornisce il valore richiesto
read.resp <- state[read.key]
case write := <-writes:
// Scrive il valore fornito
state[write.key] = write.val
// Conferma l'avvenuta scrittura
write.resp <- true
}
}
}()
// 100 goroutine che leggono
for r := 0; r < 100; r++ {
go func() {
for {
// Costruisce una struct readOp
// e ne ottiene l'indirizzo
read := &readOp{
key: rand.Intn(5),
resp: make(chan int)}
// La pone nel canale di lettura
reads <- read
// Legge il responso
<-read.resp
// Aumenta atomicamente il conto
// delle opeazioni eseguite
atomic.AddInt64(&ops, 1)
}
}()
}
// 10 goroutines che scrivono
for w := 0; w < 10; w++ {
go func() {
for {
// Costruisce la struct writeOp
// e ne ottiene l'indirizzo
write := &writeOp{
key: rand.Intn(5),
val: rand.Intn(100),
resp: make(chan bool)}
// La manda al canale di scrittura
writes <- write
// Attende il responso di avvenuto
<-write.resp
// Incrementa il contatore operazioi
atomic.AddInt64(&ops, 1)
}
}()
}
// Attendiamo un secondo
time.Sleep(time.Second)
// Vediamo il numero di operazioni eseguite
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
// Per stampare la mappa bisogna chiedere
// ogni valore alla goroutine responsabile
fmt.Print("[ ")
for c := 0; c < 5; c++ {
read := &readOp{
key: c,
resp: make(chan int)}
reads <- read
cval := <-read.resp
fmt.Printf("%d:%d ", c, cval)
}
fmt.Println("]")
}
Notare che i channel di comunicazione sono delle struct, e sono gestite per riferimento perchè è necessario cambiare il valore dei campi.
La struct di lettura ha i campi:
key
- chiave della mappa da leggereresp
- responso: valore trovato a quella chiave. E' un channel del tipo dei valori della map
La struct di scrittura ha i campi:
key
- chiave della mappa da scrivereval
- valore da scrivere a quella chiaveresp
- canale di responso, booleano, per confermare che la scrittura è avvenuta
Notare che anche il main per accedere alla mappa di stato deve seguire il protocollo corretto, chiedendo alla goroutine preposta.