Deadlock, Livelock e Starvation em Go: Entenda os 3 Vilões da Concorrência
Quando começamos a trabalhar com goroutines e mutexes em Go, tudo parece simples. Até o dia em que o programa trava sem motivo aparente, consome 100% de CPU sem fazer nada útil, ou uma goroutine simplesmente nunca consegue executar.
Esses são os três problemas clássicos de concorrência: Deadlock, Livelock e Starvation. Neste post, vou explicar cada um deles com exemplos práticos em Go que você pode rodar no seu terminal.
1. Deadlock — “Ninguém se move”
Um deadlock acontece quando duas ou mais goroutines ficam bloqueadas esperando uma pela outra, formando um ciclo de dependência. Nenhuma consegue progredir — o programa simplesmente trava.
Analogia: Duas pessoas num corredor estreito, frente a frente, cada uma recusando dar passagem. Nenhuma avança.
Exemplo em Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var muA, muB sync.Mutex
// Goroutine 1: tranca A → tenta trancar B
go func() {
muA.Lock()
fmt.Println("[G1] Trancou mutex A")
time.Sleep(100 * time.Millisecond)
fmt.Println("[G1] Tentando trancar mutex B...")
muB.Lock() // 💀 Bloqueado — G2 já tem B
defer muB.Unlock()
defer muA.Unlock()
}()
// Goroutine 2: tranca B → tenta trancar A
go func() {
muB.Lock()
fmt.Println("[G2] Trancou mutex B")
time.Sleep(100 * time.Millisecond)
fmt.Println("[G2] Tentando trancar mutex A...")
muA.Lock() // 💀 Bloqueado — G1 já tem A
defer muA.Unlock()
defer muB.Unlock()
}()
time.Sleep(3 * time.Second)
fmt.Println("Ambas as goroutines estão travadas para sempre.")
}O que acontece: G1 tranca muA e espera por muB. G2 tranca muB e espera por muA. Ninguém libera, ninguém avança — deadlock.
Como evitar
Ordene os locks: Sempre adquira mutexes na mesma ordem (A → B, nunca B → A).
Use
context.WithTimeout: Defina um prazo máximo para operações.Use canais em vez de mutexes quando possível — o modelo de concorrência do Go favorece comunicação via canais.
2. Livelock — “Todo mundo se move, ninguém avança”
Um livelock é mais traiçoeiro que um deadlock. As goroutines não estão bloqueadas — elas estão rodando, consumindo CPU, reagindo uma à outra. Mas nenhuma faz progresso real.
Analogia: Duas pessoas num corredor que tentam desviar uma da outra ao mesmo tempo, ambas indo para o mesmo lado. Elas ficam dançando para a esquerda e direita eternamente, sem nenhuma conseguir passar.
Exemplo em Go
package main
import (
"fmt"
"time"
)
func main() {
type Direction int
const (
Left Direction = 0
Right Direction = 1
)
tryToPass := func(name string, myDir *Direction, otherDir *Direction, attempts int, done chan<- bool) {
for i := 0; i < attempts; i++ {
if *myDir == *otherDir {
fmt.Printf("[%s] Bloqueado! Mesmo lado. Mudando... (tentativa %d)\n", name, i+1)
if *myDir == Left {
*myDir = Right
} else {
*myDir = Left
}
time.Sleep(50 * time.Millisecond)
} else {
fmt.Printf("[%s] Consegui passar!\n", name)
done <- true
return
}
}
fmt.Printf("[%s] Desistiu após %d tentativas!\n", name, attempts)
done <- false
}
aliceDir := Left
bobDir := Left
done := make(chan bool, 2)
go tryToPass("Alice", &aliceDir, &bobDir, 10, done)
go tryToPass("Bob", &bobDir, &aliceDir, 10, done)
<-done
<-done
fmt.Println("Nenhuma goroutine fez progresso real.")
}O que acontece: Alice e Bob verificam se estão no mesmo lado. Se sim, ambos mudam — ao mesmo tempo, para o mesmo lado. O ciclo se repete indefinidamente.
Como evitar
Introduza aleatoriedade: Adicione um
time.Sleepcom duração aleatória antes de reagir (jitter). Assim as goroutines dessincronizam.Backoff exponencial: A cada tentativa falhada, espere progressivamente mais tempo.
Prioridade: Defina uma regra de quem tem precedência (ex: goroutine com ID menor passa primeiro).
3. Starvation — “Alguém sempre fica sem comer”
Starvation (inanição) acontece quando uma goroutine consegue acesso ao recurso compartilhado com frequência muito menor que as outras. Ela não está bloqueada — simplesmente nunca (ou quase nunca) é a sua vez.
Analogia: Um buffet onde uma pessoa muito rápida sempre se serve primeiro. Quem é mais lento chega ao buffet e encontra o prato vazio, repetidamente.
Exemplo em Go
package main
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
func main() {
var mu sync.Mutex
var greedyCount, starvedCount int64
stop := make(chan struct{})
// Goroutine gulosa — re-adquire o mutex imediatamente
go func() {
for {
select {
case <-stop:
return
default:
}
mu.Lock()
atomic.AddInt64(&greedyCount, 1)
time.Sleep(1 * time.Microsecond)
mu.Unlock()
// Volta a competir sem pausa
}
}()
// Goroutine faminta — faz trabalho entre acessos
go func() {
for {
select {
case <-stop:
return
default:
}
mu.Lock()
atomic.AddInt64(&starvedCount, 1)
time.Sleep(1 * time.Microsecond)
mu.Unlock()
// Simula trabalho real entre acessos
time.Sleep(time.Duration(rand.Intn(3)+1) * time.Millisecond)
}
}()
time.Sleep(2 * time.Second)
close(stop)
time.Sleep(100 * time.Millisecond)
g := atomic.LoadInt64(&greedyCount)
s := atomic.LoadInt64(&starvedCount)
fmt.Printf("Goroutine gulosa: %d acessos\n", g)
fmt.Printf("Goroutine faminta: %d acessos\n", s)
fmt.Printf("Rácio: %.1fx mais acessos para a gulosa\n", float64(g)/float64(s))
}O que acontece: A goroutine gulosa libera o mutex e imediatamente tenta readquiri-lo. Como o scheduler do Go tende a favorecer quem já está rodando, ela monopoliza o recurso. A goroutine faminta consegue acessos esporádicos — mas muito menos.
Como evitar
Fairness com canais: Use um canal com buffer para criar uma “fila” de acesso ao recurso.
sync.Condcom broadcast: Notifique todas as goroutines esperando, não apenas uma.Rate limiting: Limite a frequência com que uma goroutine pode readquirir um recurso.
Worker pools: Distribua trabalho igualmente entre goroutines com um padrão de pool.
Resumo comparativo
🔴 Deadlock Goroutines ativas? Não — todas bloqueadas Progresso: Zero CPU: Baixo uso
🟡 Livelock Goroutines ativas? Sim — reagindo constantemente Progresso: Zero CPU: Alto uso
🟢 Starvation Goroutines ativas? Sim — mas uma é prejudicada Progresso: Parcial (desigual) CPU: Normal
Dica final
O Go tem uma filosofia clara sobre concorrência:
“Don’t communicate by sharing memory; share memory by communicating.”
Sempre que possível, prefira canais a mutexes. Canais naturalmente serializam o acesso e tornam o fluxo de dados explícito, o que reduz drasticamente a chance de cair num deadlock, livelock ou starvation.
Se precisar usar mutexes, lembre-se: adquira locks sempre na mesma ordem, use timeouts, e teste com o race detector do Go (
go run -race).Todos os exemplos deste post estão disponíveis para download e podem ser executados com
go run. Experimente modificar os timings e ver como o comportamento muda — é a melhor forma de internalizar esses conceitos.


