Pular para o conteúdo principal

victorstein.dev

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.

  1. 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
}
  1. Na função main vamos criar contas e adicionar elas ao Starbucks. Rotina bem simples que cria uma variável do tipo Starbucks 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)

	}

}
  1. 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)
}
  1. 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
  1. 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)

}
  1. 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 um WaitGroup no main, enviar ele para a função transferência, e dar o wg.Done() quando a transferência finalizar e esperar o wg chegar a 0 com o Wait()
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()
  1. 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
  1. Como podemos resolver isso? Criando um Mutex para cada conta, quando uma goroutine estiver alterando uma conta, ela dá um Lock, depois de finalizar Unlock
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)

}
  1. 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 e Unlock e veja se descobre.

  2. 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()
}
  1. 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.

  2. 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!