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.
Tente rodar o teste
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
.
Tente rodar o teste de novo e ele falhará com o seguinte erro:
Então, para finalmente fazer o teste rodar, podemos definir esses métodos:
Agora tudo deve rodar e falhar:
Escreva 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
.
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.
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
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
.
Tente rodar o teste
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.
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.
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:
Há quem diga que isso torna o código um pouco mais elegante.
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:
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
.
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.
Use essa função em seus testes quando for inicializar o Contador
.
Resumo
Falamos sobre algumas coisas do pacote sync:
Mutex
nos 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.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
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