Go

Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita

11 min de leitura

Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita

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 : o (mutex exclusivo) e o (mutex leitor-escritor). Ambos resolvem o problema, mas de formas diferentes, com trade-offs distintos. Mutex: Exclusão Mútua Simples O é a forma mais direta de proteger um recurso crítico. Ele funciona como uma porta com uma chave:

<h2>Entendendo Concorrência e a Necessidade de Sincronização</h2>

<p>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.</p>

<p>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 <code>sync</code>: o <code>Mutex</code> (mutex exclusivo) e o <code>RWMutex</code> (mutex leitor-escritor). Ambos resolvem o problema, mas de formas diferentes, com trade-offs distintos.</p>

<h2>Mutex: Exclusão Mútua Simples</h2>

<p>O <code>sync.Mutex</code> é 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 <code>Lock()</code>, a goroutine adquire o mutex; quando chama <code>Unlock()</code>, libera-o. Se outra goroutine tentar fazer <code>Lock()</code> enquanto o mutex está ocupado, ela bloqueia até que o mutex seja liberado.</p>

<h3>Estrutura Básica e Funcionamento</h3>

<p>A forma padrão de usar Mutex é envolver um recurso compartilhado com a proteção:</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;sync&quot;

)

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 := &amp;Counter{}

var wg sync.WaitGroup

// 100 goroutines incrementando o contador

for i := 0; i &lt; 100; i++ {

wg.Add(1)

go func() {

defer wg.Done()

for j := 0; j &lt; 1000; j++ {

counter.Increment()

}

}()

}

wg.Wait()

fmt.Printf(&quot;Valor final: %d (esperado: 100000)\n&quot;, counter.Value())

}</code></pre>

<p>Neste exemplo, o campo <code>mu</code> protege o campo <code>value</code>. O padrão <code>Lock()</code> seguido por <code>defer Unlock()</code> 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.</p>

<h3>Deadlock e Boas Práticas</h3>

<p>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.</p>

<pre><code class="language-go">package main

import (

&quot;sync&quot;

)

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)) &gt; 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

}</code></pre>

<h2>RWMutex: Otimizando Leituras Frequentes</h2>

<p>O <code>sync.RWMutex</code> é 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 <code>Mutex</code>, que serializa tudo, o <code>RWMutex</code> permite paralelismo quando não há modificações em progresso.</p>

<h3>Quando Usar RWMutex</h3>

<p>RWMutex oferece três operações: <code>RLock()</code> para leitura compartilhada, <code>RUnlock()</code> para liberar leitura, e <code>Lock()</code>/<code>Unlock()</code> para escrita exclusiva. A regra é simples: se você está apenas lendo, use <code>RLock()</code>; se está modificando, use <code>Lock()</code>.</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;sync&quot;

&quot;time&quot;

)

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 := &amp;Config{values: make(map[string]string)}

config.Set(&quot;database&quot;, &quot;localhost:5432&quot;)

var wg sync.WaitGroup

// 50 goroutines lendo

for i := 0; i &lt; 50; i++ {

wg.Add(1)

go func(id int) {

defer wg.Done()

for j := 0; j &lt; 100; j++ {

_ = config.Get(&quot;database&quot;)

time.Sleep(time.Microsecond)

}

}(i)

}

// 2 goroutines escrevendo ocasionalmente

for i := 0; i &lt; 2; i++ {

wg.Add(1)

go func(id int) {

defer wg.Done()

for j := 0; j &lt; 10; j++ {

time.Sleep(10 * time.Millisecond)

config.Set(&quot;database&quot;, fmt.Sprintf(&quot;server%d&quot;, id))

}

}(i)

}

wg.Wait()

fmt.Println(&quot;Teste concluído com sucesso&quot;)

}</code></pre>

<p>Neste cenário, 50 leitores podem acessar <code>Get()</code> simultaneamente enquanto nenhum escritor está ativo. Quando um escritor chama <code>Set()</code>, todos os novos <code>RLock()</code> esperam e os leitores existentes terminam antes do escritor prosseguir. Isso oferece muito mais throughput que um <code>Mutex</code> simples para este padrão de acesso.</p>

<h3>Armadilhas do RWMutex</h3>

<p>O <code>RWMutex</code> tem overhead maior que o <code>Mutex</code> simples. Se sua workload é principalmente escrita ou leitura/escrita equilibrada, um <code>Mutex</code> simples será mais rápido. Além disso, o <code>RWMutex</code> pode provocar &quot;starvation&quot; de escritores se houver um fluxo constante de leitores.</p>

<pre><code class="language-go">package main

import (

&quot;sync&quot;

&quot;testing&quot;

)

// 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()

}

})

}</code></pre>

<h2>Padrões Avançados e Erros Comuns</h2>

<p>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.</p>

<h3>Padrão de Encapsulamento</h3>

<p>A melhor prática é tornar o recurso privado (com letra minúscula) e expor apenas métodos sincronizados:</p>

<pre><code class="language-go">package main

import (

&quot;sync&quot;

)

type SafeMap struct {

mu sync.RWMutex

items map[string]interface{}

}

func NewSafeMap() *SafeMap {

return &amp;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(&quot;user:1&quot;, &quot;Alice&quot;)

sm.Set(&quot;user:2&quot;, &quot;Bob&quot;)

if val, ok := sm.Get(&quot;user:1&quot;); ok {

println(val.(string)) // Alice

}

println(sm.Len()) // 2

}</code></pre>

<h3>Evitar Erros Comuns</h3>

<p>O erro mais frequente é esquecer de chamar <code>Unlock()</code> ou esquecer o <code>Lock()</code> em uma função que deveria ser sincronizada. Use <code>defer</code> sempre que possível. Outro erro é tentar serializar o próprio mutex ou passar goroutines com comportamento impredível.</p>

<pre><code class="language-go">package main

import (

&quot;sync&quot;

)

// ❌ ERRADO: deadlock potencial

func wrongApproach() {

var mu sync.Mutex

go func() {

mu.Lock()

println(&quot;Goroutine 1&quot;)

// ... se tentar chamar outra função que faz Lock(), deadlock

}()

go func() {

mu.Lock()

println(&quot;Goroutine 2&quot;)

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(&quot;Goroutine 1&quot;)

done &lt;- true

}()

go func() {

mu.Lock()

defer mu.Unlock()

println(&quot;Goroutine 2&quot;)

done &lt;- true

}()

&lt;-done

&lt;-done

}</code></pre>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>exclusão mútua explícita via <code>sync.Mutex</code> e <code>sync.RWMutex</code> é essencial para programação concorrente segura em Go</strong>. O <code>Mutex</code> fornece proteção simples e adequada para a maioria dos casos, enquanto o <code>RWMutex</code> otimiza cenários read-heavy, permitindo leitores simultâneos. O segundo ponto crítico é que <strong>deadlocks e race conditions são evitáveis através de disciplina: use <code>defer Unlock()</code>, 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</strong>. Por fim, <strong>sempre meça e perfil seu código antes de escolher entre Mutex e RWMutex</strong>, pois o overhead do RWMutex só compensa em padrões verdadeiramente read-heavy.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://pkg.go.dev/sync" target="_blank" rel="noopener noreferrer">Go sync Package - Official Documentation</a></li>

<li><a href="https://golang.org/doc/effective_go#concurrency" target="_blank" rel="noopener noreferrer">Effective Go - Concurrency</a></li>

<li><a href="https://golang.org/ref/mem" target="_blank" rel="noopener noreferrer">The Go Memory Model</a></li>

<li><a href="https://go.dev/blog/pipelines" target="_blank" rel="noopener noreferrer">Go Concurrency Patterns</a></li>

<li><a href="https://www.oreilly.com/library/view/concurrency-in-go/9781491941294/" target="_blank" rel="noopener noreferrer">Concurrency in Go - Katherine Cox-Buday (Book)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Como Usar Structs em Go: Definição, Embedding e Métodos em Produção
Como Usar Structs em Go: Definição, Embedding e Métodos em Produção

Structs em Go: Definição, Embedding e Métodos O que é uma Struct e Por Que Us...

Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática
Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática

A Filosofia de Streams em Go A programação tradicional frequentemente trabalh...

O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução
O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução

O que é Type Switch e Por que Usar Type switch é um mecanismo em Go que permi...