Sync
Last updated
Was this helpful?
Last updated
Was this helpful?
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.
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:
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
.
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.
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.
Isso vai iterar até a nossa contagemEsperada
e disparar uma goroutine para chamar contador.Incrementa()
a cada iteração.
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
.
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.
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.
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.
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:
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
.
Mutex
nos permite adicionar travas aos nossos dados
WaitGroup
é uma maneira de esperar as goroutines terminarem suas tarefas
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.
Estamos usando , que é uma maneira simples de sincronizar processos concorrentes.
Uma solução simples é adicionar uma trava ao nosso Contador
, um .
Uma rápida olhada na documentação do nos diz o porquê:
Falamos sobre algumas coisas do :
que nos permite escrever código concorrente e seguro, então por que usar travas?