O pacote sync
Esta aula explora o pacote sync de Go, abordando WaitGroup, Mutex, RWMutex e Once para sincronização concorrente. Aprenda quando usar essas primitivas em vez de channels, com exemplos práticos e boas práticas.
O pacote sync da biblioteca padrão de Go fornece primitivas básicas de sincronização para programação concorrente. Enquanto canais (channels) são a ferramenta preferida para comunicação entre goroutines, o pacote sync oferece mecanismos para coordenar acesso a memória compartilhada e sincronizar execução. Nesta aula, exploraremos as principais funcionalidades: WaitGroup, Mutex, RWMutex e Once, entendendo quando cada uma é apropriada e como usá-las corretamente.
Dominar essas ferramentas é essencial para escrever código concorrente eficiente e seguro em Go. Embora o lema do Go seja "não se comunique por compartilhamento de memória; em vez disso, compartilhe memória se comunicando", há situações onde o uso de locks é mais simples e direto. Saber escolher entre channels e primitivas de sincronização é uma habilidade importante.
WaitGroup
sync.WaitGroup é usado para esperar que um conjunto de goroutines termine sua execução. Ele funciona como um contador: cada goroutine incrementa o contador antes de iniciar e decrementa ao finalizar. A chamada Wait() bloqueia até que o contador chegue a zero.
É comum usar Add() para definir o número de goroutines a esperar, e cada goroutine chama Done() (que equivale a Add(-1)) ao finalizar. O padrão típico é:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// trabalho da goroutine
}(i)
}
wg.Wait() // aguarda todas as goroutines
É importante chamar Add() antes de lançar a goroutine para evitar condições de corrida. O WaitGroup não deve ser copiado após o primeiro uso, pois seu estado interno deve ser compartilhado por referência.
Mutex e RWMutex
sync.Mutex é um lock exclusivo: apenas uma goroutine pode manter o lock por vez. É usado para proteger seções críticas de código que acessam dados compartilhados. O padrão é:
var mu sync.Mutex
var counter int
mu.Lock()
counter++
mu.Unlock()
Sempre use defer para garantir que o unlock seja chamado, mesmo em caso de pânico:
mu.Lock()
defer mu.Unlock()
// código crítico
sync.RWMutex é um lock de leitura/escrita que permite múltiplas leituras simultâneas, mas apenas uma escrita exclusiva. É útil quando a carga de trabalho tem muitas leituras e poucas escritas. Métodos:
RLock()/RUnlock()para leitoresLock()/Unlock()para escritores
var rw sync.RWMutex
var data map[string]int
// Leitura
rw.RLock()
value := data["chave"]
rw.RUnlock()
// Escrita
rw.Lock()
data["chave"] = 42
rw.Unlock()
O RWMutex pode ser mais eficiente que um Mutex simples quando há muitas leituras, mas cuidado: escritores podem sofrer starvation se houver muitos leitores.
Once
sync.Once garante que uma função seja executada apenas uma vez, mesmo se chamada por múltiplas goroutines. É útil para inicialização singleton ou lazy initialization.
var once sync.Once
var config *Config
func getConfig() *Config {
once.Do(func() {
config = loadConfig() // executado apenas uma vez
})
return config
}
A função passada para Do não deve depender de valores que possam mudar, e não deve causar deadlocks. Once conta o número de chamadas bem-sucedidas; se a função panic, a execução não é considerada completa, mas o Once ainda a marcará como executada? Na verdade, se a função panic, o Once considera que a execução não foi concluída e pode executar novamente em chamadas futuras? A documentação diz: "If the function panics, the Do method considers it to have returned; future calls of Do will call the function again." Portanto, é seguro usar mesmo com panics.
Quando usar em vez de channels
Go incentiva o uso de channels para comunicação entre goroutines, mas há situações onde as primitivas do pacote sync são mais apropriadas:
- Proteção de estado compartilhado: Quando múltiplas goroutines acessam uma variável ou estrutura de dados compartilhada, um
MutexouRWMutexé mais direto do que tentar serializar o acesso via channels. - Espera por conclusão:
WaitGroupé a ferramenta ideal para esperar um conjunto fixo de goroutines, sem necessidade de canal de sinalização. - Inicialização única:
Oncesimplifica padrões de lazy initialization que seriam mais complexos com channels. - Performance: Em cenários de alta contenção, locks podem ser mais rápidos que channels, embora channels sejam otimizados.
Regra prática: use channels quando houver comunicação de dados (produtor/consumidor); use primitivas de sincronização quando houver acesso concorrente a estado compartilhado. Muitas vezes, uma combinação de ambos é a melhor solução.
Boas práticas
- Nunca copie um
sync.Mutex,sync.RWMutexousync.WaitGroupapós o primeiro uso; passe por ponteiro. - Use
deferpara liberar locks, garantindo liberação mesmo em caso de pânico. - Mantenha o escopo do lock o menor possível para evitar contenção.
- Evite locks aninhados que possam causar deadlocks.
- Considere usar
go vetpara detectar possíveis problemas com cópias de mutexes.
Exercícios
-
Escreva um programa que lance 10 goroutines, cada uma incrementando um contador compartilhado 1000 vezes. Use
sync.WaitGrouppara esperar todas terminarem esync.Mutexpara proteger o contador. Imprima o valor final.✓ Resposta:package main import ( "fmt" "sync" ) func main() { var mu sync.Mutex var wg sync.WaitGroup counter := 0 for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 1000; j++ { mu.Lock() counter++ mu.Unlock() } }() } wg.Wait() fmt.Println("Contador final:", counter) } -
Implemente um cache seguro para concorrência usando
sync.RWMutex. O cache deve suportar operações de leitura (Get) e escrita (Set). Demonstre com múltiplas goroutines.✓ Resposta:package main import ( "fmt" "sync" ) type Cache struct { mu sync.RWMutex data map[string]string } func NewCache() *Cache { return &Cache{data: make(map[string]string)} } func (c *Cache) Get(key string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() val, ok := c.data[key] return val, ok } func (c *Cache) Set(key, value string) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = value } func main() { cache := NewCache() var wg sync.WaitGroup // Escritas for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)) }(i) } // Leituras for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() if val, ok := cache.Get(fmt.Sprintf("key%d", i)); ok { fmt.Printf("key%d: %s\n", i, val) } }(i) } wg.Wait() } -
Use
sync.Oncepara implementar uma função que retorna uma configuração carregada apenas uma vez. Simule um carregamento demorado comtime.Sleep.✓ Resposta:package main import ( "fmt" "sync" "time" ) type Config struct { Name string } var ( once sync.Once config *Config ) func loadConfig() *Config { fmt.Println("Carregando configuração...") time.Sleep(2 * time.Second) return &Config{Name: "MinhaConfig"} } func GetConfig() *Config { once.Do(func() { config = loadConfig() }) return config } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() cfg := GetConfig() fmt.Printf("Config: %s\n", cfg.Name) }() } wg.Wait() } -
Explique por que o código abaixo pode causar um deadlock e corrija-o usando
sync.Mutexe canais adequadamente.package main import "fmt" func main() { ch := make(chan int) go func() { ch <- 42 }() fmt.Println(<-ch) }✓ Resposta: O código original não tem deadlock, pois o canal não é bufferizado e a goroutine envia antes da recepção? Na verdade, o envio e recepção são sincronizados, então funciona. Um deadlock ocorreria se a goroutine principal tentasse receber sem uma goroutine enviando. Para exemplificar, considere:
Correção: usar uma goroutine para enviar ou usar canal bufferizado.package main import "fmt" func main() { ch := make(chan int) ch <- 42 // deadlock: nenhuma goroutine recebe fmt.Println(<-ch) } -
Crie um programa que simule um pool de workers onde cada worker processa um job e reporta o resultado. Use
sync.WaitGrouppara esperar todos os workers terminarem e umsync.Mutexpara coletar resultados em um slice compartilhado.✓ Resposta:package main import ( "fmt" "sync" ) func worker(id int, jobs <-chan int, results *[]int, mu *sync.Mutex, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { result := job * 2 // processa mu.Lock() *results = append(*results, result) mu.Unlock() fmt.Printf("Worker %d processou job %d\n", id, job) } } func main() { const numJobs = 10 const numWorkers = 3 jobs := make(chan int, numJobs) var results []int var mu sync.Mutex var wg sync.WaitGroup for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, jobs, &results, &mu, &wg) } for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs) wg.Wait() fmt.Println("Resultados:", results) }