No exemplo anterior foi utilizado travamento explícito com 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 |
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 := 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
|
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 |
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. |
Próximo exemplo: Sorting.