Stateful Goroutines
#
// No exemplo anterior foi utilizado travamento explícito com
// [mutexes](mutexes) para sincronizar acesso compartilhado a
// estados entre múltiplas goroutines. Outra opção é utilizar
// recursos de sincronização nativa das goroutines e canais
// para atingir o mesmo objetivo. Esta forma baseada em canais
// está alinhada com as ideias de comunicação através do
// compartilhamento de memória de Go, de forma que cada dado
// seja acessado por exatamente uma goroutine.
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// Neste exemplo o estado será pertencente a uma única
// goroutine (proprietária). Isso garante que o dado nunca
// seja corrompido com acesso concorrente. Para ler ou escrever
// neste estado, outras goroutines enviarão requisições para a
// goroutine proprietária e receberão as respostas correspondentes.
// As structs `readOp` e `writeOp` encapsulam estas requisições
// e uma forma para a goroutine proprietária responder.
type readOp struct {
key int
resp chan int
}
type writeOp struct {
key int
val int
resp chan bool
}
func main() {
// Serão contadas quantas operações são realizadas.
var readOps uint64
var writeOps uint64
// Os canais `reads` e `writes` serão utilizados por outras
// goroutines para emitir requisições de leitura e
// escrita respectivamente.
reads := make(chan readOp)
writes := make(chan writeOp)
// Aqui está a goroutine que possui o estado, que é um
// map, como no exemplo anterior, mas agora privado à
// _stateful goroutine_. Esta goroutine possui um
// `select` que, repetidamente verifica os canais `reads`
// e `writes`, e responde aos requests conforme são
// recebidos. A execução acontece primeiro realizando
// a operação solicitada e então enviando o valor no canal
// de respostas `resp` para indicar sucesso (e o valor desejado
// no caso de ser uma operação de leitura `reads`).
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
}
}
}()
// Este trecho inicia 100 goroutines que solicitam leituras
// para a goroutine proprietária do estado, via canal `reads`.
// Cada leitura requer a construção da struct `readOp`, bem como
// envio da operação pelo canal `reads` e, então, receber
// o resultado pelo canal `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.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// Aqui são iniciadas 10 escritas de forma similar
// à leitura.
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.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// O time.Sleep serve apenas para deixar
// as goroutines trabalharem por um segundo.
time.Sleep(time.Second)
// Finalmente, as operações realizadas são capturadas
// e a contagem reportada.
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFinal)
}
# Ao executar este código, é exibido que o exemplo de
# gerenciamento de estado baseado em goroutine completa
# cerca de 80.000 operações no total.
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177
# Para este caso em particular, o exemplo baseado em
# goroutines é um pouco mais acoplado que o baseado em
# mutex. Embora possa ser útil em alguns casos, como
# por exemplo onde exista outros canais envolvidos ou
# ao gerenciar múltiplos mutex, que seria mais propenso
# a erros. O correto é utilizar a forma que for mais
# natural, especialmente no que diz respeito à comprensão
# da forma que faça mais sentido para a realidade do
# código.