Sync
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.
Escreva o teste primeiro
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)
}
})
}Tente rodar o teste
./sync_test.go:9:14: undefined: ContadorEscreva 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 3Escreva código o suficiente para fazer o teste passar
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
}Refatoração
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)
}
}Próximos passos
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.
Escreva o teste primeiro
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
Adddefinir o número de goroutines que serão esperadas. Então, cada uma das goroutines é executada e chamaDonequando termina sua execução. Ao mesmo tempo,Waitpode 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.
Tente rodar o teste
=== 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
FAILO 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.
Escreva código o suficiente para fazer o teste passar
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.
Já vi outros exemplos em que o sync.Mutex está embutido dentro da struct.
sync.Mutex está embutido dentro da struct.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.
Copiando mutexes
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.MutexUma 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.
Resumo
Falamos sobre algumas coisas do pacote sync:
Mutexnos permite adicionar travas aos nossos dadosWaitGroupé uma maneira de esperar as goroutines terminarem suas tarefas
Quando usar travas em vez de channels e goroutines?
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.Mutexse 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
go vet
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.
Não use códigos embutidos apenas porque é conveniente
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.
Last updated
Was this helpful?