Condições de corrida e o race detector
Esta aula aborda condições de corrida (data races) em Go, explicando o que são, como detectá-las com o race detector integrado, e como prevenir usando sincronização e boas práticas de memória compartilhada.
Condições de corrida (data races) são um dos bugs mais comuns e difíceis de diagnosticar em programas concorrentes. Em Go, o modelo de concorrência baseado em goroutines e canais reduz a complexidade, mas ainda é possível introduzir races quando múltiplas goroutines acessam a mesma variável sem sincronização. Felizmente, Go oferece um detector de corridas embutido que pode ser ativado durante testes e execução.
Nesta aula, vamos explorar o que é uma data race, como usar a flag -race para detectá-la, e as principais técnicas de sincronização para evitá-la, incluindo o uso de mutexes, canais e boas práticas de memória compartilhada.
O que é data race
Uma data race ocorre quando duas ou mais goroutines acessam a mesma variável simultaneamente, e pelo menos um dos acessos é de escrita. O resultado é imprevisível: o valor lido pode estar desatualizado ou corrompido, levando a comportamentos erráticos e bugs difíceis de reproduzir.
Por exemplo, considere um contador compartilhado incrementado por várias goroutines sem sincronização. Cada incremento envolve leitura, incremento e escrita; se duas goroutines executarem isso ao mesmo tempo, uma pode sobrescrever o valor da outra.
package main
import (
"fmt"
"sync"
)
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
counter++
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter)
}O resultado esperado é 1000, mas devido à data race, o valor pode ser menor ou variar a cada execução.
go test -race
Para detectar data races, Go fornece o race detector, ativado com a flag -race em comandos como go run, go build e go test. Ele instrumenta o código para monitorar todos os acessos a memória e reportar qualquer race detectado em tempo de execução.
Para testar o programa anterior, salve-o como race.go e execute:
go run -race race.goO detector imprimirá um aviso detalhado com a localização do race, incluindo as goroutines envolvidas e as linhas de código. Em testes, use go test -race ./... para verificar todo o pacote.
Exemplo de saída:
==================
WARNING: DATA RACE
Read at 0x00c0000b4010 by goroutine 7:
main.main.func1()
.../race.go:14 +0x3e
Previous write at 0x00c0000b4010 by goroutine 6:
main.main.func1()
.../race.go:14 +0x54
Goroutine 7 (running) created at:
main.main()
.../race.go:12 +0x8a
Goroutine 6 (finished) created at:
main.main()
.../race.go:12 +0x8a
==================O detector é uma ferramenta essencial, mas tem custo de performance (cerca de 5-10x mais lento) e maior uso de memória, por isso só deve ser usado durante desenvolvimento e testes.
Sincronização
A forma mais direta de evitar data races é sincronizar o acesso a dados compartilhados. Go oferece primitivas como sync.Mutex, sync.RWMutex e canais. O uso de mutexes garante que apenas uma goroutine por vez execute uma seção crítica.
Exemplo corrigido com mutex:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter)
}Outra abordagem é usar canais para comunicação, seguindo o lema: "Não se comunique compartilhando memória; compartilhe memória se comunicando." Por exemplo, uma goroutine pode ser a única proprietária de um dado, e outras enviam requisições por canais.
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 1000; i++ {
ch <- 1
}
close(ch)
}()
sum := 0
for v := range ch {
sum += v
}
fmt.Println(sum)
}Nesse exemplo, não há variável compartilhada; a goroutine principal soma valores recebidos do canal.
Memória compartilhada
Em Go, a memória compartilhada entre goroutines exige cuidado. Sempre que múltiplas goroutines leem e escrevem na mesma variável, é necessário sincronizar. Uma boa prática é limitar o escopo dos dados compartilhados e usar tipos seguros como sync.Map ou estruturas com mutex embutido.
Evite compartilhar ponteiros entre goroutines sem sincronização. Se precisar compartilhar, considere usar um canal para transferir a posse. Por exemplo, um pool de workers pode receber tarefas por um canal e enviar resultados por outro, sem compartilhar estado.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
var wg sync.WaitGroup
c := &Counter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
fmt.Println(c.Value())
}Outra dica: use go vet para detectar possíveis races em tempo de compilação (embora limitado). E lembre-se: o race detector só encontra races que ocorrem durante a execução; testes exaustivos com concorrência são recomendados.
Referências
- Documentação oficial do Race Detector
- The Go Blog: Introducing the Go Race Detector
- The Go Memory Model
- Package sync documentation
- Go Playground
Exercícios
- Crie um programa que lance 10 goroutines, cada uma incrementando um contador compartilhado 100 vezes sem sincronização. Execute com
go run -racee observe o aviso. Corrija o programa usando um mutex. - Modifique o programa do exercício 1 para usar um canal com buffer de tamanho 1 como mutex. Dica: envie um token para o canal antes de acessar o contador e retire após.
- Escreva um teste para uma função que retorna o valor de um contador após incrementos concorrentes. Use
go test -racepara verificar se há data race. A função deve usar sincronização. - Explique por que o seguinte código pode ter uma data race e corrija-o usando canais: duas goroutines escrevem em um mapa compartilhado sem sincronização.
- Crie um programa que simule um banco com múltiplas contas e transferências concorrentes. Use um mutex por conta para evitar races. Execute com
-racepara garantir que está correto.
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 100; j++ {
mu.Lock()
counter++
mu.Unlock()
}
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter)
}package main
import (
"fmt"
"sync"
)
var counter int
var sem = make(chan struct{}, 1)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 100; j++ {
sem <- struct{}{}
counter++
<-sem
}
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter)
}package counter
import (
"sync"
"testing"
)
type SafeCounter struct {
mu sync.Mutex
n int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func TestConcurrentIncrement(t *testing.T) {
c := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
if c.Value() != 100 {
t.Errorf("esperado 100, obtido %d", c.Value())
}
}package main
import "fmt"
type op struct {
key string
value int
resp chan struct{}
}
func main() {
ch := make(chan op)
m := make(map[string]int)
go func() {
for o := range ch {
m[o.key] = o.value
o.resp <- struct{}{}
}
}()
for i := 0; i < 10; i++ {
go func(i int) {
resp := make(chan struct{})
ch <- op{key: fmt.Sprintf("key%d", i), value: i, resp: resp}
<-resp
}(i)
}
}package main
import (
"fmt"
"sync"
)
type Account struct {
mu sync.Mutex
balance int
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
a.balance += amount
a.mu.Unlock()
}
func (a *Account) Withdraw(amount int) bool {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance >= amount {
a.balance -= amount
return true
}
return false
}
func Transfer(from, to *Account, amount int) bool {
if from.Withdraw(amount) {
to.Deposit(amount)
return true
}
return false
}
func main() {
alice := &Account{balance: 1000}
bob := &Account{balance: 500}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
Transfer(alice, bob, 10)
wg.Done()
}()
}
wg.Wait()
fmt.Println("Alice:", alice.balance, "Bob:", bob.balance)
}