Condições de corrida e o race detector
Nesta aula, você aprenderá o que são condições de corrida (data races) em Go, como detectá-las com a ferramenta race detector e como evitá-las usando sincronização adequada. Abordaremos o comando 'go test -race', os mecanismos de sincronização como canais e mutexes, e a filosofia de 'compartilhar memória comunicando-se'.
Condições de corrida (data races) são um dos bugs mais sutis e perigosos em programas concorrentes. Em Go, a linguagem oferece ferramentas poderosas para detectá-las e preveni-las. Nesta aula, vamos explorar o que é uma data race, como usar o race detector integrado, as técnicas de sincronização e a abordagem de memória compartilhada via comunicação.
Entender e evitar condições de corrida é essencial para escrever código concorrente seguro e confiável. Vamos mergulhar nos conceitos e práticas que todo desenvolvedor Go deve dominar.
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. Isso leva a comportamentos imprevisíveis, pois o resultado depende da ordem de execução não determinística das goroutines.
Em Go, uma data race é uma condição de corrida que envolve acesso concorrente a memória compartilhada sem sincronização adequada. O modelo de memória de Go define que, sem sincronização, não há garantia de que uma goroutine veja as alterações feitas por outra.
Exemplo clássico de data race:
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() {
defer wg.Done()
counter++ // data race: acesso concorrente sem sincronização
}()
}
wg.Wait()
fmt.Println(counter)
}Neste código, múltiplas goroutines incrementam a variável counter simultaneamente. O resultado esperado seria 1000, mas devido à data race, o valor final pode ser menor e variar a cada execução.
go test -race
Go inclui um detector de corridas (race detector) integrado que pode ser ativado com a flag -race nos comandos go run, go build e go test. Ele instrumenta o código para detectar acessos concorrentes a memória compartilhada em tempo de execução.
Para usar com testes, execute:
go test -race ./...O detector reporta cada data race encontrada, mostrando o stack trace das goroutines envolvidas. É uma ferramenta essencial para identificar problemas de concorrência durante o desenvolvimento.
Exemplo de saída do race detector:
WARNING: DATA RACE
Read at 0x00c0000a0008 by goroutine 8:
main.main.func1()
/path/to/main.go:12 +0x3a
Previous write at 0x00c0000a0008 by goroutine 7:
main.main.func1()
/path/to/main.go:12 +0x50
Goroutine 8 (running) created at:
main.main()
/path/to/main.go:10 +0x47
Goroutine 7 (finished) created at:
main.main()
/path/to/main.go:10 +0x47O detector é ativado em tempo de execução, portanto, para detectar todas as raças, é necessário que o código seja executado com cargas de trabalho que exercitem os caminhos concorrentes.
Sincronização
A sincronização em Go pode ser feita usando canais (channels) ou primitivas de sincronização do pacote sync, como Mutex e WaitGroup. A escolha depende do padrão de concorrência desejado.
Uso de Mutex:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
v int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.v++
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v
}
func main() {
var wg sync.WaitGroup
c := SafeCounter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println(c.Value())
}Uso de 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)
}Ambos os exemplos evitam data races: o primeiro usando mutex para proteger o acesso à variável compartilhada, o segundo usando canais para comunicação e evitando compartilhamento direto de memória.
Memória compartilhada
O lema de Go para concorrência é: "Não se comunique compartilhando memória; em vez disso, compartilhe memória comunicando-se." Isso significa que, idealmente, você deve usar canais para passar dados entre goroutines, em vez de acessar diretamente variáveis compartilhadas.
No entanto, há situações em que o compartilhamento de memória com sincronização é mais apropriado, como em estruturas de dados de alto desempenho ou quando o padrão de acesso é complexo. Nesses casos, use sync.Mutex ou sync.RWMutex para proteger os acessos.
Exemplo com RWMutex:
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (sm *SafeMap) Get(key string) int {
sm.mu.RLock()
defer sm.mu.RUnlock()
return sm.m[key]
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func main() {
sm := SafeMap{m: make(map[string]int)}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key%d", i), i)
}()
}
wg.Wait()
fmt.Println(sm.Get("key5"))
}Lembre-se de que o race detector só encontra raças que realmente ocorrem durante a execução. Portanto, é importante escrever testes que exerçam a concorrência de forma abrangente.
Boas práticas
Sempre execute seus testes e programas com a flag -race durante o desenvolvimento. Evite compartilhar memória sempre que possível; prefira canais. Se precisar compartilhar, use primitivas de sincronização adequadas e mantenha o escopo de bloqueio o menor possível.
Além disso, evite padrões como sync/atomic para iniciantes, a menos que você entenda profundamente as barreiras de memória. O race detector é seu aliado, mas não substitui um bom design.
Referências
- Documentação oficial do Race Detector
- Artigo do blog Go sobre Race Detector
- Modelo de Memória de Go
- Pacote sync
- Code Walk: Compartilhando Memória
- Tour de Concorrência em Go
Exercícios
O código a seguir contém uma data race. Identifique-a e corrija usando um mutex.
package main import ( "fmt" "sync" ) var count int func main() { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() count++ }() } wg.Wait() fmt.Println(count) }✓ Resposta: Adicione um mutex para proteger o acesso acount:package main import ( "fmt" "sync" ) var ( count int mu sync.Mutex ) func main() { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() count++ mu.Unlock() }() } wg.Wait() fmt.Println(count) }Reescreva o programa do exercício 1 usando canais para evitar data races, sem usar mutex.
✓ Resposta:package main import ( "fmt" ) func main() { ch := make(chan int) go func() { for i := 0; i < 100; i++ { ch <- 1 } close(ch) }() sum := 0 for v := range ch { sum += v } fmt.Println(sum) }O que o comando
go test -race ./...faz? Explique em uma frase.✓ Resposta: Ele executa todos os testes do pacote atual e subpacotes com o detector de corridas ativado, que reporta acessos concorrentes a memória compartilhada.Escreva um programa que cria 10 goroutines, cada uma escrevendo em um mapa compartilhado, protegido por
sync.RWMutex. Use o race detector para verificar se há data races.✓ Resposta:package main import ( "fmt" "sync" ) type SafeMap struct { mu sync.RWMutex m map[int]int } func (sm *SafeMap) Set(key, value int) { sm.mu.Lock() defer sm.mu.Unlock() sm.m[key] = value } func main() { sm := SafeMap{m: make(map[int]int)} var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() sm.Set(i, i*2) }() } wg.Wait() fmt.Println(sm.m) }Explique a diferença entre
sync.Mutexesync.RWMutexe quando usar cada um.✓ Resposta:sync.Mutexfornece bloqueio exclusivo: apenas uma goroutine pode acessar o recurso por vez.sync.RWMutexpermite múltiplas leituras simultâneas, mas apenas uma escrita exclusiva. Usesync.RWMutexquando houver muitas leituras e poucas escritas, pois melhora a performance.