Entendendo Concorrência e a Necessidade de Sincronização
A programação concorrente em Go permite que múltiplas goroutines executem simultaneamente, compartilhando recursos como variáveis, estruturas de dados e conexões com banco de dados. Quando várias goroutines acessam o mesmo recurso ao mesmo tempo, surgem problemas graves: race conditions, corrupção de dados e comportamentos impredíveis. A exclusão mútua (mutex) é o mecanismo fundamental para garantir que apenas uma goroutine acesse um recurso crítico por vez, preservando a integridade dos dados.
Considere um exemplo real: você tem um contador compartilhado que múltiplas goroutines incrementam simultaneamente. Sem sincronização, duas goroutines poderiam ler o mesmo valor, incrementar e escrever de volta, causando perda de incrementos. O Go fornece dois tipos de mutex na package sync: o Mutex (mutex exclusivo) e o RWMutex (mutex leitor-escritor). Ambos resolvem o problema, mas de formas diferentes, com trade-offs distintos.
Mutex: Exclusão Mútua Simples
O sync.Mutex é a forma mais direta de proteger um recurso crítico. Ele funciona como uma porta com uma chave: apenas a goroutine que possui a chave pode entrar na seção crítica. Quando você chama Lock(), a goroutine adquire o mutex; quando chama Unlock(), libera-o. Se outra goroutine tentar fazer Lock() enquanto o mutex está ocupado, ela bloqueia até que o mutex seja liberado.
Estrutura Básica e Funcionamento
A forma padrão de usar Mutex é envolver um recurso compartilhado com a proteção:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
// 100 goroutines incrementando o contador
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Increment()
}
}()
}
wg.Wait()
fmt.Printf("Valor final: %d (esperado: 100000)\n", counter.Value())
}
Neste exemplo, o campo mu protege o campo value. O padrão Lock() seguido por defer Unlock() garante que o mutex seja sempre liberado, mesmo se um pânico ocorrer. Sem essa proteção, o resultado final seria impredívelmente menor que 100000 devido à race condition.
Deadlock e Boas Práticas
Um risco comum ao usar Mutex é o deadlock: quando uma goroutine tenta adquirir um mutex que ela mesma já possui (em Go, isso causará um pânico). Outro cenário é quando goroutines A e B esperam uma pela outra indefinidamente. A melhor prática é manter seções críticas pequenas e evitar chamar funções que também tentam adquirir o mesmo mutex.
package main
import (
"sync"
)
type BankAccount struct {
mu sync.Mutex
balance float64
}
func (ba *BankAccount) Transfer(other *BankAccount, amount float64) {
// ⚠️ PERIGO: se outro Transfer chamar Transfer na ordem inversa, deadlock!
ba.mu.Lock()
defer ba.mu.Unlock()
other.mu.Lock()
defer other.mu.Unlock()
ba.balance -= amount
other.balance += amount
}
// ✓ Solução: usar ordem consistente
func (ba *BankAccount) TransferSafe(other *BankAccount, amount float64) {
// Sempre fazer lock na conta com ID menor primeiro
first, second := ba, other
if uintptr(unsafe.Pointer(ba)) > uintptr(unsafe.Pointer(other)) {
first, second = other, ba
}
first.mu.Lock()
defer first.mu.Unlock()
second.mu.Lock()
defer second.mu.Unlock()
ba.balance -= amount
other.balance += amount
}
RWMutex: Otimizando Leituras Frequentes
O sync.RWMutex é um mutex leitor-escritor que permite múltiplos leitores simultâneos, mas apenas um escritor exclusivo. Isso é ideal quando seu padrão de acesso é muito mais leitura do que escrita. Diferentemente do Mutex, que serializa tudo, o RWMutex permite paralelismo quando não há modificações em progresso.
Quando Usar RWMutex
RWMutex oferece três operações: RLock() para leitura compartilhada, RUnlock() para liberar leitura, e Lock()/Unlock() para escrita exclusiva. A regra é simples: se você está apenas lendo, use RLock(); se está modificando, use Lock().
package main
import (
"fmt"
"sync"
"time"
)
type Config struct {
mu sync.RWMutex
values map[string]string
}
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.values[key]
}
func (c *Config) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.values[key] = value
}
func main() {
config := &Config{values: make(map[string]string)}
config.Set("database", "localhost:5432")
var wg sync.WaitGroup
// 50 goroutines lendo
for i := 0; i < 50; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
_ = config.Get("database")
time.Sleep(time.Microsecond)
}
}(i)
}
// 2 goroutines escrevendo ocasionalmente
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
time.Sleep(10 * time.Millisecond)
config.Set("database", fmt.Sprintf("server%d", id))
}
}(i)
}
wg.Wait()
fmt.Println("Teste concluído com sucesso")
}
Neste cenário, 50 leitores podem acessar Get() simultaneamente enquanto nenhum escritor está ativo. Quando um escritor chama Set(), todos os novos RLock() esperam e os leitores existentes terminam antes do escritor prosseguir. Isso oferece muito mais throughput que um Mutex simples para este padrão de acesso.
Armadilhas do RWMutex
O RWMutex tem overhead maior que o Mutex simples. Se sua workload é principalmente escrita ou leitura/escrita equilibrada, um Mutex simples será mais rápido. Além disso, o RWMutex pode provocar "starvation" de escritores se houver um fluxo constante de leitores.
package main
import (
"sync"
"testing"
)
// Demonstra quando Mutex é melhor que RWMutex
func BenchmarkMutex(b *testing.B) {
var mu sync.Mutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
value++
mu.Unlock()
}
})
}
func BenchmarkRWMutex(b *testing.B) {
var mu sync.RWMutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
value++
mu.Unlock()
}
})
}
// Para aplicações com padrão read-heavy, RWMutex vence
func BenchmarkMutexReadHeavy(b *testing.B) {
var mu sync.Mutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
_ = value
mu.Unlock()
}
})
}
func BenchmarkRWMutexReadHeavy(b *testing.B) {
var mu sync.RWMutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.RLock()
_ = value
mu.RUnlock()
}
})
}
Padrões Avançados e Erros Comuns
Além do uso básico, existem padrões que aumentam a robustez do código concorrente. Um padrão fundamental é separar a estrutura de dados da lógica que a protege, garantindo que o mutex seja sempre usado quando necessário.
Padrão de Encapsulamento
A melhor prática é tornar o recurso privado (com letra minúscula) e expor apenas métodos sincronizados:
package main
import (
"sync"
)
type SafeMap struct {
mu sync.RWMutex
items map[string]interface{}
}
func NewSafeMap() *SafeMap {
return &SafeMap{items: make(map[string]interface{})}
}
func (sm *SafeMap) Set(key string, value interface{}) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.items[key] = value
}
func (sm *SafeMap) Get(key string) (interface{}, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
val, ok := sm.items[key]
return val, ok
}
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.items, key)
}
func (sm *SafeMap) Len() int {
sm.mu.RLock()
defer sm.mu.RUnlock()
return len(sm.items)
}
func main() {
sm := NewSafeMap()
sm.Set("user:1", "Alice")
sm.Set("user:2", "Bob")
if val, ok := sm.Get("user:1"); ok {
println(val.(string)) // Alice
}
println(sm.Len()) // 2
}
Evitar Erros Comuns
O erro mais frequente é esquecer de chamar Unlock() ou esquecer o Lock() em uma função que deveria ser sincronizada. Use defer sempre que possível. Outro erro é tentar serializar o próprio mutex ou passar goroutines com comportamento impredível.
package main
import (
"sync"
)
// ❌ ERRADO: deadlock potencial
func wrongApproach() {
var mu sync.Mutex
go func() {
mu.Lock()
println("Goroutine 1")
// ... se tentar chamar outra função que faz Lock(), deadlock
}()
go func() {
mu.Lock()
println("Goroutine 2")
mu.Unlock()
}()
}
// ✓ CORRETO: usar canais ou passar o mutex para funções que precisam
func correctApproach() {
var mu sync.Mutex
done := make(chan bool, 2)
go func() {
mu.Lock()
defer mu.Unlock()
println("Goroutine 1")
done <- true
}()
go func() {
mu.Lock()
defer mu.Unlock()
println("Goroutine 2")
done <- true
}()
<-done
<-done
}
Conclusão
Aprendemos que exclusão mútua explícita via sync.Mutex e sync.RWMutex é essencial para programação concorrente segura em Go. O Mutex fornece proteção simples e adequada para a maioria dos casos, enquanto o RWMutex otimiza cenários read-heavy, permitindo leitores simultâneos. O segundo ponto crítico é que deadlocks e race conditions são evitáveis através de disciplina: use defer Unlock(), mantenha seções críticas pequenas, estabeleça uma ordem consistente para múltiplos locks, e encapsule recursos compartilhados em tipos com métodos sincronizados. Por fim, sempre meça e perfil seu código antes de escolher entre Mutex e RWMutex, pois o overhead do RWMutex só compensa em padrões verdadeiramente read-heavy.