Você pode encontrar todo o código para esse capítulo aqui
Queremos fazer um contador que é seguro para ser usado concorrentemente.
Vamos começar com um contador não seguro e verificar se seu comportamento funciona em um ambiente com apenas uma thread.
Em seguida, vamos testar sua falta de segurança com várias goroutines tentando usar o contador dentro dos testes e consertar essa falha.
Queremos que nossa API nos dê um método para incrementar o contador e depois recupere esse valor.
func TestContador(t *testing.T) {
t.Run("incrementar o contador 3 vezes resulta no valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
contador.Incrementa()
if contador.Valor() != 3 {
t.Errorf("resultado %d, esperado %d", contador.Valor(), 3)
}
})
}
./sync_test.go:9:14: undefined: Contador
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Vamos definir Contador
.
type Contador struct {
}
Tente rodar o teste de novo e ele falhará com o seguinte erro:
./sync_test.go:14:10: contador.Incrementa undefined (type Contador has no field or method Incrementa)
./sync_test.go:18:13: contador.Valor undefined (type Contador has no field or method Valor)
Então, para finalmente fazer o teste rodar, podemos definir esses métodos:
func (c *Contador) Incrementa() {
}
func (c *Contador) Valor() int {
return 0
}
Agora tudo deve rodar e falhar:
=== RUN TestContador
=== RUN TestContador/incrementar_o_contador_3_vezes_resulta_no_valor_3
--- FAIL: TestContador (0.00s)
--- FAIL: TestContador/incrementar_o_contador_3_vezes_resulta_no_valor_3 (0.00s)
sync_test.go:27: resultado 0, esperado 3
Isso deve ser simples para experts em Go como nós. Precisamos criar uma instância do tipo Contador e incrementá-lo com cada chamada de Incrementa
.
type Contador struct {
valor int
}
func (c *Contador) Incrementa() {
c.valor++
}
func (c *Contador) Valor() int {
return c.valor
}
Não há muito o que refatorar, mas já que iremos escrever mais testes em torno do Contador
, vamos escrever uma pequena função de asserção verificaContador
para que o teste fique um pouco mais legível.
t.Run("incrementar o contador 3 vezes resulta no valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
contador.Incrementa()
verificaContador(t, contador, 3)
})
func verificaContador(t *testing.T, resultado Contador, esperado int) {
t.Helper()
if resultado.Valor() != esperado {
t.Errorf("resultado %d, esperado %d", resultado.Valor(), esperado)
}
}
Isso foi muito fácil, mas agora temos um requerimento que é: o programa precisa ser seguro o suficiente para ser usado em um ambiente com acesso concorrente. Vamos precisar criar um teste para exercitar isso.
t.Run("roda concorrentemente em segurança", func(t *testing.T) {
contagemEsperada := 1000
contador := Contador{}
var wg sync.WaitGroup
wg.Add(contagemEsperada)
for i := 0; i < contagemEsperada; i++ {
go func(w *sync.WaitGroup) {
contador.Incrementa()
w.Done()
}(&wg)
}
wg.Wait()
verificaContador(t, contador, contagemEsperada)
})
Isso vai iterar até a nossa contagemEsperada
e disparar uma goroutine para chamar contador.Incrementa()
a cada iteração.
Estamos usando sync.WaitGroup
, que é uma maneira simples de sincronizar processos concorrentes.
Um WaitGroup aguarda por uma coleção de goroutines terminar seu processamento. A goroutine principal faz a chamada para o
Add
definir o número de goroutines que serão esperadas. Então, cada uma das goroutines é executada e chamaDone
quando termina sua execução. Ao mesmo tempo,Wait
pode ser usado para bloquear a execução até que todas as goroutines tenham terminado.
Ao esperar por wg.Wait()
terminar sua execução antes de fazer nossas asserções, podemos ter certeza que todas as nossas goroutines tentaram chamar o Incrementa
no Contador
.
=== RUN TestContador/roda_concorrentemente_em_seguranca
--- FAIL: TestContador (0.00s)
--- FAIL: TestContador/roda_concorrentemente_em_seguranca (0.00s)
sync_test.go:26: resultado 939, esperado 1000
FAIL
O teste provavelmente vai falhar com um número diferente, mas de qualquer forma demonstra que não roda corretamente quando várias goroutines tentam mudar o valor do contador ao mesmo tempo.
Uma solução simples é adicionar uma trava ao nosso Contador
, um Mutex
.
Um Mutex é uma trava de exclusão mútua. O valor zero de um Mutex é um Mutex destravado.
type Contador struct {
mu sync.Mutex
valor int
}
func (c *Contador) Incrementa() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
Isso significa que qualquer goroutine chamando Incrementa
vai receber a trava em Contador
se for a primeira chamando essa função. Todas as outras goroutines vão ter que esperar por essa primeira execução até que ele esteja Unlock
, ou destravado, antes de ganhar o acesso à instância de Contador
alterada pela primeira chamada de função.
Agora, se você rodar o teste novamente, ele deve funcionar porque cada uma das goroutines tem que esperar até que seja sua vez antes de fazer alguma mudança.
Você pode ver exemplos como esse:
type Contador struct {
sync.Mutex
valor int
}
Há quem diga que isso torna o código um pouco mais elegante.
func (c *Contador) Incrementa() {
c.Lock()
defer c.Unlock()
c.valor++
}
Isso parece legal, mas, apesar de programação ser uma área altamente subjetiva, isso é feio e errado.
Às vezes as pessoas esquecem que tipos embutidos significam que os métodos daquele tipo se tornam parte da interface pública; e você geralmente não quer isso. Não se esqueçam que devemos ter muito cuidado com as nossas APIs públicas. O momento que tornamos algo público é o momento que outros códigos podem acoplar-se a ele e queremos evitar acoplamentos desnecessários.
Expôr Lock
e Unlock
é, no seu melhor caso, muito confuso e, no seu pior caso, potencialmente perigoso para o seu software se quem chamar o seu tipo começar a chamar esses métodos diretamente.
Isso parece uma péssima ideia.
Nossos testes passam, mas nosso código ainda é um pouco perigoso.
Se você rodar go vet
no seu código, deve receber um erro similar ao seguinte:
sync/v2/sync_test.go:16: call of verificaContador copies lock valor: v1.Contador contains sync.Mutex
sync/v2/sync_test.go:39: verificaContador passes lock by valor: v1.Contador contains sync.Mutex
Uma rápida olhada na documentação do sync.Mutex
nos diz o porquê:
Um Mutex não deve ser copiado depois do primeiro uso.
Quando passamos nosso Contador
(por valor) para verificaContador
, ele vai tentar criar uma cópia do mutex.
Para resolver isso, devemos passar um ponteiro para o nosso Contador
. Vamos, então, mudar a assinatura de verificaContador
.
func verificaContador(t *testing.T, resultado *Contador, esperado int)
Nossos testes não vão mais compilar porque estamos tentando passar um Contador
ao invés de um *Contador
. Para resolver isso, é melhor criar um construtor que mostra aos usuários da nossa API que seria melhor ele mesmo não inicializar seu tipo.
func NovoContador() *Contador {
return &Contador{}
}
Use essa função em seus testes quando for inicializar o Contador
.
Falamos sobre algumas coisas do pacote sync:
Mutex
nos permite adicionar travas aos nossos dadosWaitGroup
é uma maneira de esperar as goroutines terminarem suas tarefas
Anteriormente falamos sobre goroutines no primeiro capítulo sobre concorrência que nos permite escrever código concorrente e seguro, então por que usar travas? A wiki do Go tem uma página dedicada para esse tópico: Mutex ou Channel?
Um erro comum de um iniciante em Go é usar demais os channels e goroutines apenas porque é possível e/ou porque é divertido. Não tenha medo de usar um
sync.Mutex
se for uma solução melhor para o seu problema. Go é pragmático em deixar você escolher as ferramentas que melhor resolvem o seu problema e não te força em um único estilo de código.
Resumindo:
- Use channels quando for passar a propriedade de um dado
- Use mutexes para gerenciar estados
Não se esqueça de usar go vet
nos seus scripts de build porque ele pode te alertar a respeito de bugs mais sutis no seu código antes que eles atinjam seus pobres usuários.
- Pense a respeito do efeito que embutir códigos tem na sua API pública.
- Você realmente quer expôr esses métodos e ter pessoas acoplando o código próprio delas a ele?
- Mutexes podem se tornar um desastre de maneiras muito imprevisíveis e estranhas. Imagine um código inesperado destravando um mutex quando não deveria? Isso causaria erros muito estranhos que seriam muito difíceis de encontrar.