Protegendo dados compartilhados
Um sync.Mutex deixa uma goroutine de cada vez entrar no código entre Lock e Unlock. Coloque o mutex ao lado dos dados que ele protege, normalmente na mesma struct:
Isto sempre imprime hits: 10000. Sem o lock, 100 goroutines escrevendo no mesmo map normalmente derrubariam o programa com fatal error: concurrent map writes. Esse erro vem de uma verificação de melhor esforço do runtime, não pode ser recuperado, e a ausência de crash não prova que o código está correto.
Detalhes que importam:
- O valor zero de
sync.Mutexestá destravado e pronto. Sem construtor. - Os métodos usam receiver ponteiro (
*Counter). Um receiver de valor travaria uma cópia do mutex, o que não protege nada. defer c.mu.Unlock()logo depois doLockfaz com que todo caminho de retorno, e um panic, liberem o lock.- Todo acesso passa pelo lock, inclusive as leituras. Uma leitura sem o lock enquanto outra goroutine escreve continua sendo uma data race.
Mantenha a seção crítica pequena
O defer destrava no fim da função. Isso está certo para métodos curtos como os de cima. Em uma função mais longa, destrave assim que os dados compartilhados não forem mais tocados, para que outras goroutines não fiquem esperando por um trabalho que não precisa do lock:
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
Segurar um lock durante chamadas de rede, I/O de disco ou um envio em channel é a causa mais comum de um programa concorrente lento, e um envio em channel com o lock travado é uma causa comum de deadlock.
RWMutex para dados com muita leitura
sync.RWMutex tem dois modos. RLock/RUnlock pegam um lock de leitura compartilhado que muitas goroutines podem segurar ao mesmo tempo. Lock/Unlock pegam o lock exclusivo de escrita, que espera todos os leitores saírem.
O RWMutex compensa quando as leituras dominam e cada leitura faz trabalho real sob o lock. Em seções críticas minúsculas, como uma única busca em map, um Mutex simples costuma ser igualmente rápido, porque o lock de leitura tem a sua própria contabilidade. Faça benchmark antes de escolher.
Não dá para promover um lock de leitura a lock de escrita. Chamar Lock enquanto segura RLock na mesma goroutine causa deadlock. Libere antes o lock de leitura, depois pegue o de escrita e verifique a condição de novo, já que outro escritor pode ter mudado os dados nesse meio-tempo.
sync/atomic para valores únicos
Para um contador ou uma flag, sync/atomic é mais simples e mais barato que um mutex. Os wrappers tipados (Go 1.19) são os que você deve usar:
Atomics protegem um valor de cada vez. Assim que dois valores precisam mudar juntos (um saldo e uma contagem de transações, um map e o seu tamanho), use um mutex. Duas operações atômicas separadas podem se intercalar com outras goroutines entre elas.
sync.Once
O sync.Once executa uma função exatamente uma vez, não importa quantas goroutines o chamem ao mesmo tempo. Todos que chamam Do esperam até a primeira chamada terminar. É o jeito padrão de inicializar algo de forma preguiçosa:
Cada linha "runs once" aparece exatamente uma vez. Se a função passada para Do causar panic, o Once a considera feita mesmo assim e nunca tenta de novo. sync.OnceValues faz o mesmo para funções que devolvem dois valores, normalmente um valor e um erro.
Mutex, channel ou sync.Map
| Situação | Use |
|---|---|
| Uma struct ou map que várias goroutines atualizam no lugar | sync.Mutex na struct |
| Principalmente leituras, escritas ocasionais, leituras fazem trabalho real | sync.RWMutex |
| Um único contador ou flag | sync/atomic |
| Inicialização única | sync.Once, sync.OnceValue |
| Passar dados de uma goroutine para outra | um channel |
| Um cache cujas chaves são escritas uma vez e lidas muitas vezes, ou goroutines mexendo em chaves disjuntas | sync.Map |
O sync.Map não é um substituto geral para um map com lock. Ele não tem parâmetros de tipo, então os valores voltam como any, e ele só é mais rápido nos dois casos da tabela. Comece com um mutex e um map comum.
Erros comuns
- Copiar um mutex. Passar por valor uma struct que contém um
sync.Mutex, ou usar um receiver de valor, copia o lock. Ogo vetreportapasses lock by valueoucopies lock value. - Travar duas vezes na mesma goroutine. Mutexes em Go não são reentrantes. Se
IncchamaGete os dois pegam o lock,Incbloqueia para sempre. Faça os métodos públicos travarem e as funções auxiliares privadas assumirem que o lock já está travado. - Esquecer de destravar em um retorno antecipado. Use
defer, a menos que tenha um motivo para não usar. - Travar em ordens diferentes. Se uma goroutine pega o lock A e depois o B enquanto outra pega B e depois A, as duas podem ficar esperando uma pela outra para sempre. Sempre adquira vários locks na mesma ordem.
- Expor os dados protegidos. Devolver o map interno em um método permite que quem chama o leia e escreva sem o lock. Devolva uma cópia (
maps.Clone, Go 1.21) ou um único valor. - Proteger só as escritas. Leituras sem lock concorrentes com escritas com lock continuam sendo races. Execute os seus testes com
go test -race.
Perguntas frequentes
O que é um mutex em Go?
Um sync.Mutex é um lock que só deixa uma goroutine de cada vez executar o código entre mu.Lock() e mu.Unlock(). Você o usa para proteger dados que várias goroutines leem e escrevem, como um map ou uma struct. O seu valor zero é um mutex destravado, pronto para uso.
Quando usar RWMutex em vez de Mutex?
Quando as leituras superam muito as escritas e cada leitura segura o lock por um tempo significativo. RLock deixa entrar qualquer número de leitores ao mesmo tempo, enquanto Lock espera por acesso exclusivo. Em seções críticas curtas, um Mutex simples muitas vezes é tão rápido quanto ou mais, então meça antes de trocar.
Um map em Go é seguro para uso concorrente?
Não. Leituras concorrentes não têm problema, mas uma escrita concorrente com qualquer outra leitura ou escrita é uma data race, e o runtime normalmente a detecta e derruba o programa com fatal error: concurrent map writes (ou concurrent map read and map write). Proteja o map com um sync.Mutex ou sync.RWMutex, ou use sync.Map nos casos de uso específicos dele.
O sync.Mutex é reentrante em Go?
Não. Se uma goroutine que segura o lock chama Lock de novo, ela bloqueia para sempre esperando por si mesma. Estruture o código para que os métodos exportados peguem o lock e chamem funções auxiliares não exportadas que assumem que ele já está travado.