20/100 Dias de Golang - Exemplo de goroutines, mutex e channels
Table of Contents
#
Resolvendo o problema do Starbucks
Vamos “resolver” o problema do Starbucks. Para quem não sabe do que estou falando: Problema de race condition no starbucks afeta gift cards. Vamos criar um programa bem simples para simular isso e já aproveitamos para treinar os conhecimentos até aqui. Então o que teremos: pessoas com saldo nas contas e uma função que você pode transferir saldo do gift card de uma conta para outra. Vou fazer o programa de forma evolucional, começando bem simples e incrementar aos poucos.
- Vamos criar duas structs. Uma simulando a conta e outra simulando o Starbucks.
type Conta struct {
ID int
SaldoGiftCards int
}
type Starbucks struct {
Contas []*Conta
}
- Na função
main
vamos criar contas e adicionar elas aoStarbucks
. Rotina bem simples que cria uma variável do tipoStarbucks
e faz um range criando 5 contas, cada conta foi adicionada com um saldo aleatório até 100. E já adiciono um print para verificar os valores
func main() {
starbucks := Starbucks{}
for i := 0; i < 5; i++ {
starbucks.Contas = append(starbucks.Contas, &Conta{ID: i, SaldoGiftCards: rand.Intn(100) + 1000})
fmt.Printf("Conta %d: %d\n", banco.Contas[i].ID, banco.Contas[i].SaldoGiftCards)
}
}
- Agora temos que criar uma função de transferência. Ela será associada a struct do
Starbucks
, de forma bem simples, ela recebe dois ID de conta e um valor a ser transferido. Como vou fazer tudo com o rand.Intn vou adicionar uma verficação se origem é a mesma que destino
func (b *Starbucks) Transferir(origem, destino, valor int) {
if origem == destino {
return
}
contaOrigem := b.Contas[origem]
contaDestino := b.Contas[destino]
fmt.Printf("Transferência de %d da conta %d para a conta %d", valor, contaOrigem.ID, contaDestino.ID)
}
- Vamos rodar isso no
main
. Criando 10 transações aleatórias.
func main() {
starbucks := Starbucks{}
for i := 0; i < 5; i++ {
starbucks.Contas = append(starbucks.Contas, &Conta{ID: i, SaldoGiftCards: rand.Intn(100)})
fmt.Printf("Conta %d: %d\n", starbucks.Contas[i].ID, starbucks.Contas[i].SaldoGiftCards)
}
for i := 0; i < 10; i++ {
starbucks.Transferir(rand.Intn(5), rand.Intn(5), rand.Intn(100))
}
}
Saída:
Conta 0: 92 Conta 1: 44 Conta 2: 74 Conta 3: 51 Conta 4: 45 Transferência de R$36 da conta 0 para a conta 3 Transferência de R$49 da conta 3 para a conta 0 Transferência de R$75 da conta 0 para a conta 4 Transferência de R$9 da conta 4 para a conta 3 Transferência de R$97 da conta 1 para a conta 4 Transferência de R$20 da conta 2 para a conta 3 Transferência de R$28 da conta 3 para a conta 0 Transferência de R$27 da conta 0 para a conta 2 Transferência de R$20 da conta 1 para a conta 0
- Veja na saída
Transferência de R$97 da conta 1 para a conta 4
, mas a conta 1 nem possui o saldo de 97, vamos adicionar uma verificação. E já aproveitamos para adicionar a atualização dos saldos nas contas.
func (b *Starbucks) Transferir(origem, destino, valor int) {
if origem == destino {
return
}
contaOrigem := b.Contas[origem]
contaDestino := b.Contas[destino]
if contaOrigem.SaldoGiftCards >= valor {
contaOrigem.SaldoGiftCards -= valor
contaDestino.SaldoGiftCards += valor
fmt.Printf("Transferência de R$%d da conta %d para a conta %d\n", valor, contaOrigem.ID, contaDestino.ID)
}
fmt.Printf("Saldo insuficiente na conta %d para a transferência de R$%d para a conta %d\n", contaOrigem.ID, valor, contaDestino.ID)
}
- Agora sim! Como estamos vendo as goroutines, vamos fazer com que as trânsferências sejam executadas cada uma por uma goroutine. Para isso teremos que adicionar um
WaitGroup
para esperar todas as goroutines terminarem. Para isso teremos que criar umWaitGroup
nomain
, enviar ele para a função transferência, e dar owg.Done()
quando a transferência finalizar e esperar owg
chegar a 0 com oWait()
var wg sync.WaitGroup
func (b *Starbucks) Transferir(origem, destino, valor int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10; i++ {
wg.Add(1)
go starbucks.Transferir(rand.Intn(5), rand.Intn(5), rand.Intn(100), &wg)
}
wg.Wait()
- Se você rodar esse programa com a flag de verificação de racecondition, verá que pode aparecer um erro. O que esse erro nos diz A goroutine 8 está lendo um valor na memória dentro da função Transferir(). Simultaneamente, a goroutine 9 está escrevendo nessa mesma região de memória.
go run --race main.go Conta 0: 2 Conta 1: 81 Conta 2: 99 Conta 3: 60 Conta 4: 6 Transferência de R$1 da conta 2 para a conta 3 Saldo insuficiente na conta 2 para a transferência de R$1 para a conta 3 ================== WARNING: DATA RACE Read at 0x00c0000a00d8 by goroutine 8: main.(*Starbucks).Transferir() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:29 +0x195 main.main.gowrap1() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:51 +0x64 Previous write at 0x00c0000a00d8 by goroutine 9: main.(*Starbucks).Transferir() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:30 +0x1d4 main.main.gowrap1() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:51 +0x64 Goroutine 8 (running) created at: main.main() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:51 +0x3ae Goroutine 9 (running) created at: main.main() /home/victorstein/Cursos/golang-victor/channels/dia20/main.go:51 +0x3ae
- Como podemos resolver isso? Criando um Mutex para cada conta, quando uma goroutine estiver alterando uma conta, ela dá um
Lock
, depois de finalizarUnlock
type Conta struct {
ID int
SaldoGiftCards int
Mutex sync.Mutex
}
func (b *Starbucks) Transferir(origem, destino, valor int, wg *sync.WaitGroup) {
defer wg.Done()
if origem == destino {
return
}
contaOrigem := b.Contas[origem]
contaDestino := b.Contas[destino]
contaOrigem.Mutex.Lock()
contaDestino.Mutex.Lock()
if contaOrigem.SaldoGiftCards >= valor {
contaOrigem.SaldoGiftCards -= valor
contaDestino.SaldoGiftCards += valor
fmt.Printf("Transferência de R$%d da conta %d para a conta %d\n", valor, contaOrigem.ID, contaDestino.ID)
}
contaOrigem.Mutex.Unlock()
contaDestino.Mutex.Unlock()
fmt.Printf("Saldo insuficiente na conta %d para a transferência de R$%d para a conta %d\n", contaOrigem.ID, valor, contaDestino.ID)
}
-
Rodei duas vezes o programa completo, na primeira vez foi ok, na segunda aconteceu uma coisa estranha. O sistema simplesmente ficou travado. Dê uma olhada na implementação do
Lock
eUnlock
e veja se descobre. -
Acontece que como rodamos as transferências com dados aleatórios, podemos rodar uma transferência A com a conta de origem 1 e destino 2 e outra transferência B com a conta de origem 2 e destino 1. Nesse caso irá acontecer o seguinte, a transferência A vai lockar a conta de origem 1 e vai tentar lockar a conta de origem 2, porém a transferência B já deu lock na conta 2 e quer dar lock na 1. E o que acontece? Uma fica esperando a outra liberar a conta de destino, mas ambas precisam de um recurso da outra que já está locado. Bem confuso, mas vamos implementar uma lógica bem simples para resolver isso. Veja que interessante. Vamos dar lock com base no ID da conta, com isso garantimos sempre a mesma ordem de lock.
if origem < destino {
contaOrigem.Mutex.Lock()
contaDestino.Mutex.Lock()
} else {
contaDestino.Mutex.Lock()
contaOrigem.Mutex.Lock()
}
-
Agora sim! Rodei várias vezes e o erro não ocorreu. Agora só para brincar com os canais, vamos enviar as mensagens da função de transferência por um canal. Vamos implementar o canal na main e enviar por parâmetro para a função transferir.
-
Veja o código completo:
package main
import (
"fmt"
"math/rand"
"sync"
)
type Conta struct {
ID int
SaldoGiftCards int
Mutex sync.Mutex
}
type Starbucks struct {
Contas []*Conta
}
func (b *Starbucks) Transferir(origem, destino, valor int, wg *sync.WaitGroup, transacoes chan string) {
defer wg.Done()
if origem == destino {
return
}
contaOrigem := b.Contas[origem]
contaDestino := b.Contas[destino]
if origem < destino {
contaOrigem.Mutex.Lock()
contaDestino.Mutex.Lock()
} else {
contaDestino.Mutex.Lock()
contaOrigem.Mutex.Lock()
}
if contaOrigem.SaldoGiftCards >= valor {
contaOrigem.SaldoGiftCards -= valor
contaDestino.SaldoGiftCards += valor
msg := fmt.Sprintf("Transferência de R$%d da conta %d para a conta %d\n", valor, contaOrigem.ID, contaDestino.ID)
transacoes <- msg
}
contaOrigem.Mutex.Unlock()
contaDestino.Mutex.Unlock()
msg := fmt.Sprintf("Saldo insuficiente na conta %d para a transferência de R$%d para a conta %d\n", contaOrigem.ID, valor, contaDestino.ID)
transacoes <- msg
}
func main() {
starbucks := Starbucks{}
for i := 0; i < 5; i++ {
starbucks.Contas = append(starbucks.Contas, &Conta{ID: i, SaldoGiftCards: rand.Intn(100)})
fmt.Printf("Conta %d: %d\n", starbucks.Contas[i].ID, starbucks.Contas[i].SaldoGiftCards)
}
var wg sync.WaitGroup
transacoes := make(chan string)
for i := 0; i < 10; i++ {
wg.Add(1)
go starbucks.Transferir(rand.Intn(5), rand.Intn(5), rand.Intn(100), &wg, transacoes)
}
go func() {
wg.Wait()
close(transacoes)
}()
fmt.Println("\nTransações realizadas:")
for transacao := range transacoes {
fmt.Println(transacao)
}
for i := 0; i < 5; i++ {
fmt.Printf("Conta %d: %d\n", starbucks.Contas[i].ID, starbucks.Contas[i].SaldoGiftCards)
}
}
Um exemplo bem simples apenas para colocarmos o conhecimento em prática!