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 leggere
  • resp - 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 scrivere
  • val - valore da scrivere a quella chiave
  • resp - 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.