Sync package: Mutex, RWMutex e WaitGroup
1. Introdução ao pacote sync
O pacote sync é um dos pilares da programação concorrente em Go, oferecendo primitivas de sincronização de baixo nível para coordenar o acesso a recursos compartilhados entre goroutines. Enquanto o modelo de concorrência do Go é frequentemente associado aos canais (CSP), o pacote sync resolve problemas específicos onde a exclusão mútua e a sincronização de estado são necessárias.
Os problemas comuns que o pacote sync aborda incluem:
- Race conditions: quando múltiplas goroutines acessam e modificam dados simultaneamente sem sincronização
- Deadlocks: situações onde goroutines bloqueiam umas às outras indefinidamente
- Sincronização de término: garantir que goroutines concluam antes de prosseguir
A escolha entre sync e canais depende do contexto: canais são ideais para comunicação e fluxo de dados, enquanto sync.Mutex e sync.RWMutex são mais adequados para proteger estado compartilhado.
2. sync.Mutex: Exclusão Mútua Básica
O sync.Mutex implementa exclusão mútua, garantindo que apenas uma goroutine execute uma seção crítica por vez. Seu uso é direto com os métodos Lock() e Unlock().
type Contador struct {
mu sync.Mutex
valor int
}
func (c *Contador) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
func (c *Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
Armadilhas comuns incluem:
- Deadlocks: ocorrem quando uma goroutine tenta adquirir um lock já mantido por ela mesma
- Locks não liberados: esquecer Unlock() em todos os caminhos de código, inclusive em erros
- Locks aninhados: adquirir múltiplos locks em ordem inconsistente entre goroutines
O padrão defer é essencial para garantir que Unlock() seja chamado mesmo em caso de panic ou retorno prematuro.
3. sync.RWMutex: Leitura e Escrita com Performance
O sync.RWMutex otimiza cenários onde leituras são muito mais frequentes que escritas. Ele permite que múltiplas goroutines adquiram locks de leitura simultaneamente, enquanto locks de escrita têm exclusão total.
type CacheConcorrente struct {
mu sync.RWMutex
dados map[string]string
}
func (c *CacheConcorrente) Get(chave string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
valor, ok := c.dados[chave]
return valor, ok
}
func (c *CacheConcorrente) Set(chave, valor string) {
c.mu.Lock()
defer c.mu.Unlock()
c.dados[chave] = valor
}
Quando usar RWMutex vs. Mutex:
- Use sync.RWMutex quando houver muito mais leituras que escritas
- Use sync.Mutex para simplicidade ou quando escritas forem frequentes
- O overhead do RWMutex pode piorar a performance se escritas forem comuns
4. sync.WaitGroup: Sincronização de Goroutines
O sync.WaitGroup é um contador que aguarda até que um conjunto de goroutines conclua sua execução. Seu funcionamento é baseado em três métodos: Add(), Done() e Wait().
func ProcessarLotes(dados []int) {
var wg sync.WaitGroup
for _, item := range dados {
wg.Add(1)
go func(val int) {
defer wg.Done()
processarItem(val)
}(item)
}
wg.Wait()
fmt.Println("Todos os itens processados")
}
Cuidados essenciais:
- Chame Add() antes de iniciar a goroutine, não dentro dela
- Evite valores negativos no contador (causam panic)
- Passe o WaitGroup por ponteiro para funções que lançam goroutines
5. Padrões Avançados com sync.Mutex e sync.RWMutex
Mutex protegendo mapas compartilhados:
type MapaSeguro struct {
mu sync.Mutex
data map[string]int
}
func (m *MapaSeguro) OperacaoAtomica(chave string, fn func(*int)) {
m.mu.Lock()
defer m.mu.Unlock()
valor := m.data[chave]
fn(&valor)
m.data[chave] = valor
}
Combinação com defer para garantir liberação em panics:
func (c *CacheConcorrente) SetWithPanic(chave, valor string) {
c.mu.Lock()
defer func() {
c.mu.Unlock()
if r := recover(); r != nil {
log.Printf("Recuperado de panic: %v", r)
}
}()
// Operação que pode causar panic
validarValor(valor)
c.dados[chave] = valor
}
6. Padrões Avançados com sync.WaitGroup
WaitGroup com loops e slices de goroutines:
func ProcessarConcorrente(tarefas []Tarefa) {
var wg sync.WaitGroup
errChan := make(chan error, len(tarefas))
for i := range tarefas {
wg.Add(1)
go func(t Tarefa) {
defer wg.Done()
if err := t.Executar(); err != nil {
errChan <- err
}
}(tarefas[i])
}
wg.Wait()
close(errChan)
for err := range errChan {
log.Printf("Erro: %v", err)
}
}
WaitGroup como mecanismo de barreira simples:
type Barreira struct {
wg sync.WaitGroup
}
func (b *Barreira) Esperar() {
b.wg.Wait()
}
func (b *Barreira) Registrar(n int) {
b.wg.Add(n)
}
func (b *Barreira) Liberar() {
b.wg.Done()
}
7. Erros Comuns e Boas Práticas
Erro: Copiar valor de Mutex ou WaitGroup
// ERRADO: copia o Mutex
type EstruturaErrada struct {
mu sync.Mutex
}
func (e EstruturaErrada) Metodo() { // Passagem por valor copia o Mutex
e.mu.Lock()
defer e.mu.Unlock()
// ...
}
// CORRETO: usa ponteiro
type EstruturaCorreta struct {
mu sync.Mutex
}
func (e *EstruturaCorreta) Metodo() {
e.mu.Lock()
defer e.mu.Unlock()
// ...
}
Mutex dentro de struct:
- Use ponteiro quando a struct for passada por valor
- Use valor embutido quando a struct for sempre usada por ponteiro
- Nunca copie uma struct que contém um Mutex
8. Comparação com Outras Abordagens de Concorrência
Mutex vs. Canais:
Os Go Proverbs sugerem: "Don't communicate by sharing memory; instead, share memory by communicating." Na prática:
- Use canais para transferir dados entre goroutines
- Use Mutex para proteger estado compartilhado complexo
- Use RWMutex para otimizar leituras frequentes
WaitGroup vs. canais de sincronização:
// Usando WaitGroup
var wg sync.WaitGroup
for _, t := range tarefas {
wg.Add(1)
go func() {
defer wg.Done()
t()
}()
}
wg.Wait()
// Usando done channel
done := make(chan struct{})
go func() {
for _, t := range tarefas {
t()
}
close(done)
}()
<-done
Limitações do pacote sync:
- sync.Mutex não é reentrante (causa deadlock)
- sync/atomic é mais performático para operações simples
- context é melhor para cancelamento e timeouts
- Para filas e pipelines, canais são mais expressivos
O pacote sync continua sendo a ferramenta certa para sincronização de estado compartilhado em Go, especialmente quando combinado com boas práticas como defer e encapsulamento adequado.
Referências
- Documentação oficial do pacote sync — Referência completa de todas as primitivas de sincronização do Go, incluindo Mutex, RWMutex e WaitGroup
- Go by Example: Mutexes — Exemplos práticos de uso de Mutex e RWMutex em Go com código comentado
- The Go Memory Model — Documentação oficial sobre o modelo de memória do Go, essencial para entender garantias de sincronização
- Dave Cheney: Practical Go - Concurrency — Palestra técnica com padrões avançados de concorrência em Go
- Golang Concurrency Patterns: Context — Artigo oficial sobre o pacote context, alternativa para sincronização e cancelamento em Go
- RWMutex vs Mutex performance benchmarks — Análise detalhada de performance comparando Mutex e RWMutex em diferentes cenários