Pular para o conteúdo

Trilha de Go · parte 18 de 65

Race condition em Go: o que é e como o detector de corrida ajuda

Race condition em Go acontece quando a ordem entre goroutines muda o resultado. Veja como ler o relatório do -race e corrigir com mutex ou canal.

Publicada em · 23 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Uma , ou condição de corrida, acontece quando o resultado de um programa depende da ordem em que eventos concorrentes acontecem, e pelo menos uma dessas ordens produz um resultado errado. Em Go, o caso mais comum é o de duas ou mais goroutines lendo e escrevendo a mesma variável sem combinar nada entre si.

Esta aula ensina o que é a corrida, por que ela escapa dos testes comuns, a diferença entre race condition e , o que já vem com a toolchain (go run -race e go test -race), duas formas de corrigir o problema, com e com , e por que o detector só enxerga o que executa. Ao final, você deve conseguir olhar um trecho concorrente e apontar quais acessos estão sem coordenação, em vez de confiar que o teste passou.

Pré-requisito: Goroutine, para saber o que é lançar trabalho concorrente. Os exemplos usam waitGroup para esperar as goroutines terminarem. Esta aula não aprofunda o modelo de memória. Esse assunto e as primitivas de ficam na aula de sync.

Mapa da aula

ParteSeçõesO que você vai aprender
1. Fundamentos e intuição1 a 2O que é uma race condition e por que a ordem dos eventos deixou de ser previsível
2. Race condition e data race3 a 4A diferença entre os dois termos e um contador que perde incrementos
3. O detector de corrida5 a 6go run -race e go test -race, o que o relatório diz e quanto ele custa
4. Corrigir com mutex e canal7 a 8A trava que protege o acesso e a goroutine
5. Os limites do detector9 a 10Race condition sem data race e por que o silêncio não prova nada
6. Prática e consolidação11 a 15Exercícios com gabarito, glossário, o que não foi confirmado, fontes e relacionados

Parte 1: Fundamentos e intuição

1. O que é uma race condition

Pense no último pão de queijo da bandeja numa festa. Duas pessoas estendem a mão ao mesmo tempo. Quem chega primeiro leva, e a outra fica com a mão no ar olhando para a bandeja vazia. Ninguém errou, a bandeja não quebrou e a contagem dos pães não ficou torta. O resultado apenas dependeu de quem chegou antes. Se a cena se repetir dez vezes, a vencedora pode mudar a cada vez. Isso é uma race condition: um resultado que depende da ordem ou do tempo relativo de eventos que não foram coordenados.

O ponto que importa é a coordenação. Se o resultado correto não depende da ordem, não há corrida. Se algum resultado possível está errado e nada impede que ele aconteça, há corrida. Em código sequencial, uma linha só começa depois que a anterior terminou, então a ordem vem de graça. Com duas goroutines, a ordem entre as linhas de uma e de outra não é garantida. Cabe a você decidir quais ordens são aceitáveis e criar a regra que as impõe.

Um termo precisa de definição curta já aqui: data race é um caso mais técnico de corrida, em que duas goroutines acessam a mesma posição de memória ao mesmo tempo, sem sincronização entre os acessos, e pelo menos um deles é uma escrita. A versão completa, com a diferença entre os dois termos, fica na seção 3.

2. Por que a ordem dos eventos deixou de ser previsível

Antes de haver várias linhas de execução, a ordem das coisas era a ordem do código. A concorrência surgiu para aproveitar os núcleos da CPU e para não deixar o programa parado esperando disco ou rede. A partir daí, quem decide qual goroutine roda e quando é o runtime do Go, por meio do Scheduler. Em uma máquina com vários núcleos, duas goroutines podem executar no mesmo instante, o que é paralelismo. Nenhuma dessas decisões aparece no código, e ambas mudam de uma execução para outra conforme a carga, o número de núcleos e o momento em que cada goroutine é colocada para rodar.

Isso explica por que a corrida engana. O código parece correto, o teste local passa, e o erro só aparece quando a calha de acontecer. Quem nunca viu um bug desses provavelmente ainda não rodou o código com carga de verdade.

Pense em ouro++. A linha parece um passo só, mas a máquina costuma executá-la em três etapas: ler o valor atual, somar 1 e escrever o resultado de volta. Se duas goroutines executam essas etapas ao mesmo tempo, elas podem se intercalar de formas que a linha escondida no código não mostra. A linguagem não faz ouro++ ser uma operação indivisível. Para isso existem os mutexes e o pacote sync/atomic, tratados nas seções 7 e 8 e na aula de sync. O problema existe em outras linguagens também: as Thread do sistema operacional enfrentam o mesmo desafio. O Go oferece primitivas de coordenação e uma ferramenta de diagnóstico. Sozinha, nenhuma das duas elimina o problema.

Parte 2: Race condition e data race

3. Race condition e data race: dois termos, duas ideias

Os dois termos aparecem misturados em tutoriais e em revisões de código. Vale separá-los, porque a ferramenta que o Go oferece enxerga apenas um deles.

A documentação oficial do detector define a data race assim, em tradução livre: ela ocorre quando duas goroutines acessam a mesma variável ao mesmo tempo e pelo menos um desses acessos é uma escrita. O que falta nessa definição, e que vem da teoria de concorrência, é a ausência de sincronização entre os dois acessos. Sem ela, nenhuma ordem entre eles está garantida. O modelo de memória do Go só garante ordem entre acessos ligados por uma sincronização. Entre os demais, não há garantia alguma, e o detalhe fica para a aula de sync.

A race condition é uma situação de lógica. Ela descreve qualquer resultado que dependa da ordem e que possa estar errado. A regra de negócio decide se um resultado está errado. Por isso os dois conceitos não coincidem. Uma data race pode ou não produzir um resultado visivelmente errado, e uma race condition pode existir sem nenhuma data race. O exemplo da reserva, na seção 9, mostra esse segundo caso: cada acesso à memória está protegido e a regra de negócio, mesmo assim, quebra.

A distinção importa porque o detector de corrida procura data races. Ele não conhece a sua regra de negócio. Um bug de reserva pode existir com zero alertas, e um teste verde com o detector ligado só diz que aquele caminho não cometeu data race.

4. Um contador que perde incrementos

O programa abaixo cria mil goroutines, e cada uma soma 1 mil vezes na mesma variável. O resultado deveria ser um milhão, mas nada garante esse valor, e o programa não emite erro algum.

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	const goroutines = 1000
	const incrementos = 1000

	var ouro int
	var wg sync.WaitGroup

	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < incrementos; j++ {
				ouro++
			}
		}()
	}

	wg.Wait()
	esperado := goroutines * incrementos
	fmt.Printf("esperado: %d\n", esperado)
	fmt.Printf("obtido:   %d\n", ouro)
	fmt.Printf("perdidos: %d (GOMAXPROCS=%d)\n", esperado-ouro, runtime.GOMAXPROCS(0))
}

O diagrama abaixo mostra uma das intercalações possíveis. O valor começa em 10 só para facilitar a leitura, porque o programa real começa em zero. A faixa amarela marca o trecho em que as duas goroutines leem antes de qualquer escrita. Essa é a janela da corrida. Repare que a escrita da goroutine B apaga o +1 que a goroutine A já tinha feito. Nada no código sinaliza isso, e a perda some sem erro.

Duas goroutines leem o mesmo ouro (10) antes de qualquer escrita, e a segunda escrita apaga o +1 da primeira: o resultado é 11 em vez de 12.
figura 1 · em um contador sem proteção

Rodei o programa três vezes sem o detector, em um MacBook Pro com Apple M1 Max e dez núcleos lógicos (GOMAXPROCS=10). Os resultados foram estes:

esperado: 1000000
obtido:   533058
perdidos: 466942 (GOMAXPROCS=10)

esperado: 1000000
obtido:   586416
perdidos: 413584 (GOMAXPROCS=10)

esperado: 1000000
obtido:   1000000
perdidos: 0 (GOMAXPROCS=10)

A terceira execução acertou, e isso não prova nada: a corrida é intermitente. Em oito execuções sem o detector, as perdas variaram de 0 a 885 736. Este exemplo usa mil goroutines fazendo mil somas cada uma para tornar o problema visível. Com menos concorrência, o erro pode ficar escondido por muito tempo, e um teste curto não vai pegá-lo.

Parte 3: O detector de corrida

5. O detector de corrida: go run -race e go test -race

O Go traz o detector de corrida no próprio compilador. A flag -race instrumenta cada leitura e cada escrita de memória do programa e registra qual goroutine tocou em cada endereço e se havia uma ordem de sincronização entre os acessos. Sincronização, aqui, é qualquer mecanismo que estabelece uma ordem entre dois acessos, como um mutex ou um canal. Quando dois acessos concorrentes não têm essa ordem e pelo menos um deles é escrita, o detector emite um aviso. A ferramenta é derivada do ThreadSanitizer, uma biblioteca de análise dinâmica do Google, segundo o post oficial que apresentou o detector em 2013.

Os comandos mais usados são estes:

go run -race ./contador-bug
go test -race ./...
go build -race -o app-diagnostico .

O custo precisa ser conhecido antes de qualquer decisão. A documentação oficial estima, para um programa típico, de 5 a 10 vezes mais memória e de 2 a 20 vezes mais tempo de execução. Por isso o binário com -race serve para testes e para a integração contínua, e não para a produção. Num pipeline, rodar go test -race ./... é um bom padrão para os pacotes que têm goroutines.

Quando o detector encontra uma corrida, o programa termina com código de saída 66 por padrão. O go run mostra isso como exit status 66. A flag funciona em darwin/arm64, segundo go help build na toolchain que usei nesta revisão.

6. Lendo o relatório: uma execução com race e uma sem

Esta é a saída real do go run -race no contador sem proteção. Os caminhos foram encurtados para contador-bug/main.go, e o detector emitiu dois blocos; mostro o primeiro:

==================
WARNING: DATA RACE
Read at 0x00c000012138 by goroutine 53:
  main.main.func1()
      contador-bug/main.go:21 +0x88

Previous write at 0x00c000012138 by goroutine 131:
  main.main.func1()
      contador-bug/main.go:21 +0x98

Goroutine 53 (running) created at:
  main.main()
      contador-bug/main.go:18 +0x70

Goroutine 131 (finished) created at:
  main.main()
      contador-bug/main.go:18 +0x70
==================
...
esperado: 1000000
obtido:   695532
perdidos: 304468 (GOMAXPROCS=10)
Found 2 data race(s)
exit status 66

Leia o relatório de cima para baixo. A primeira linha diz o tipo do problema. Em seguida vêm a leitura (Read at) e a escrita anterior (Previous write at), cada uma com a goroutine que a fez e a linha do código. O bloco created at mostra onde cada goroutine foi criada, que é o ponto de partida para descobrir quem disparou o trabalho. Os dois endereços são idênticos, o que confirma que se trata da mesma variável. A linha 21 do arquivo é ouro++, e a linha 18 é o go func().

Repare que o programa imprimiu o resultado errado depois do aviso. O detector relata a corrida e deixa o programa seguir.

Esta é a saída do mesmo contador depois da correção com mutex, rodada com -race:

esperado: 1000000
obtido:   1000000
perdidos: 0 (GOMAXPROCS=10)

O exit status foi 0 e nenhum aviso apareceu. Um relatório limpo significa que o detector não encontrou corrida nos acessos que rodaram. A seção 10 explica por que essa frase tem limite.

Parte 4: Corrigir com mutex e canal

7. Corrigir com mutex

O mutex é a chave do banheiro de um posto de gasolina. Só entra quem pegou a chave, e quem esquece de devolvê-la deixa a fila inteira cruzando as pernas. Na prática, Lock bloqueia a goroutine até que outra chame Unlock. O trecho entre os dois é chamado de , porque só uma goroutine por vez pode executá-lo.

A versão corrigida muda apenas a declaração das variáveis e o corpo do laço. O restante do programa é igual ao anterior:

var (
	mu   sync.Mutex
	ouro int
	wg   sync.WaitGroup
)

for i := 0; i < goroutines; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for j := 0; j < incrementos; j++ {
			mu.Lock()
			ouro++
			mu.Unlock()
		}
	}()
}

A trava protege a variável apenas se todo acesso passa por ela. Uma única função que esquece o Lock derruba a proteção de todas as outras, como o caso de SomarRapido na seção 10. Dentro de funções, o padrão mais seguro é mu.Lock() seguido de defer mu.Unlock() na linha de baixo, para que um retorno antecipado não deixe a trava presa. Esquecer o Unlock é um erro caro, e quem nunca deixou uma trava presa ainda não teve um deadlock às três da manhã. A documentação do pacote sync também avisa que um Mutex não deve ser copiado depois do primeiro uso, o que pode ser verificado com go doc sync.Mutex na toolchain instalada.

A trava tem custo. Goroutines que disputam o mesmo mutex esperam umas pelas outras. Para um contador isolado a espera é pequena, mas em regiões críticas longas ela aparece no tempo de resposta.

8. Corrigir com canal: uma goroutine dona do estado

Imagine um restaurante com uma única pessoa na chapa. Os garçons não encostam na chapa. Eles anotam o pedido num papel e entregam a ela, que é a única a mexer na comida. Num programa, a mesma ideia aparece quando uma goroutine é dona de uma variável e as outras só enviam pedidos para ela por um canal:

pedidos := make(chan int)
resultado := make(chan int)

go func() {
	ouro := 0
	for soma := range pedidos {
		ouro += soma
	}
	resultado <- ouro
}()

var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for j := 0; j < incrementos; j++ {
			pedidos <- 1
		}
	}()
}

wg.Wait()
close(pedidos)
ouro := <-resultado

O trecho acima é o centro do programa completo, que foi rodado com e sem -race. Nele, o canal pedidos é sem buffer. Isso quer dizer que quem envia espera até a dona receber, e o próprio canal controla o ritmo. A goroutine dona é a única que toca em ouro, por isso a variável não precisa de trava. Os produtores só conhecem o canal.

A ordem das operações de fechamento importa. O close(pedidos) só pode vir depois de wg.Wait(), porque enviar para um canal já fechado causa pânico. A especificação da linguagem diz, em tradução livre, que enviar ou fechar um canal fechado causa um pânico em tempo de execução. Todo mundo esquece o close uma vez: sem ele, o range da dona nunca termina e <-resultado espera para sempre.

A saída com -race foi esta, idêntica à do mutex:

esperado: 1000000
obtido:   1000000
perdidos: 0 (GOMAXPROCS=10)
Três produtores enviam 1 por um canal sem buffer, e uma única goroutine dona soma no ouro. Só a dona toca na variável, então ela não precisa de trava.
figura 2 · Canal com uma única goroutine dona do ouro, sem trava na variável

No diagrama, cada produtor envia um pedido e o canal segura um pedido por vez. A caixa da dona carrega o selo “Única”, que resume a regra: só ela escreve. A seta laranja, de dona para ouro, é o único caminho de escrita, e por isso a caixa do ouro não aparece com trava. Os pacotes azuis são os pedidos dos produtores.

A escolha entre mutex e canal depende da forma do problema. Use mutex quando o estado é simples e a região protegida é curta, como um contador ou um mapa de cache. Use canal quando existe um fluxo de trabalho, com produtores, consumidores e um dono do estado que serializa as mudanças. Um canal usado só para imitar uma trava costuma ficar mais complicado que o mutex que ele substitui. Para um contador isolado, sync/atomic também resolve, mas esse caminho fica para a aula de sync.

Parte 5: Os limites do detector

9. Race condition sem data race: o caso da reserva

Este programa não tem nenhuma data race. Todo acesso ao mapa passa pelo mutex mu. Mesmo assim, Ana e Bruno reservam a mesma poltrona:

package main

import (
	"fmt"
	"sync"
	"time"
)

// Assentos guarda quem ocupa cada poltrona. Todo acesso ao mapa passa pelo mutex,
// então o detector de corrida não tem o que reclamar.
type Assentos struct {
	mu       sync.Mutex
	ocupados map[string]string
}

func (a *Assentos) Livre(assento string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	_, ocupado := a.ocupados[assento]
	return !ocupado
}

func (a *Assentos) Reservar(assento, passageiro string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.ocupados[assento] = passageiro
}

func main() {
	a := &Assentos{ocupados: map[string]string{}}
	var wg sync.WaitGroup

	for _, p := range []string{"Ana", "Bruno"} {
		wg.Add(1)
		go func(passageiro string) {
			defer wg.Done()
			// Checar e reservar são duas travas separadas. Entre elas, a outra goroutine pode agir.
			if a.Livre("12A") {
				time.Sleep(10 * time.Millisecond) // só alarga a janela para o efeito aparecer sempre
				a.Reservar("12A", passageiro)
				fmt.Printf("%s reservou 12A\n", passageiro)
			} else {
				fmt.Printf("%s viu 12A ocupado\n", passageiro)
			}
		}(p)
	}

	wg.Wait()
	fmt.Printf("ocupante final de 12A: %s\n", a.ocupados["12A"])
}

A saída com -race, em uma das execuções, foi esta:

Bruno reservou 12A
Ana reservou 12A
ocupante final de 12A: Ana

Não houve aviso, e o exit status foi 0. Em 20 execuções seguidas, as duas goroutines reservaram a poltrona em todas. A ordem das linhas e o ocupante final variaram. O erro está entre Livre e Reservar: são duas travas separadas, e entre elas a outra goroutine pode agir. A espera de 10 milissegundos existe só para tornar o efeito estável nesta demonstração.

Duas goroutines checam a poltrona 12A antes de reservar. Cada acesso tem trava, mas checar e reservar são travas separadas, e as duas reservas passam.
figura 3 · Reserva dupla com cada acesso protegido por trava, mas a sequência não

O diagrama mostra a mesma situação na ordem do tempo. A faixa vermelha marca a janela entre a checagem e a reserva, em que as duas respostas são “livre”. Cada acesso ao mapa passou por uma trava, então nenhum deles é uma data race. A regra de negócio, porém, foi quebrada.

A correção é colocar a checagem e a escrita dentro da mesma trava, para que nenhuma outra goroutine entre no meio:

// ReservarSeLivre checa e escreve dentro da mesma trava: nenhuma outra goroutine entra no meio.
func (a *Assentos) ReservarSeLivre(assento, passageiro string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	if _, ocupado := a.ocupados[assento]; ocupado {
		return false
	}
	time.Sleep(10 * time.Millisecond) // a mesma janela de antes, agora fechada pela trava
	a.ocupados[assento] = passageiro
	return true
}

Com essa versão, em 20 execuções houve sempre uma reserva e um “viu ocupado”. A saída típica com -race foi:

Bruno viu 12A ocupado
Ana reservou 12A
ocupante final de 12A: Ana

O detector respondeu à pergunta que sabe responder: dois acessos à mesma memória se intercalaram sem ordem? A pergunta sobre a regra de negócio, se a poltrona pode ter dois donos, ficou com quem escreve o código.

10. Ausência de alerta não prova ausência de corrida

Este é um erro comum com o detector. Considere um pacote com dois caminhos de escrita: Somar, que usa mutex, e SomarRapido, que esqueceu a trava. Ninguém escreveu teste para o segundo caminho.

var (
	mu    sync.Mutex
	total int
)

// Somar é o caminho normal: protegido pelo mutex.
func Somar(n int) {
	mu.Lock()
	total += n
	mu.Unlock()
}

// SomarRapido é um caminho secundário que esqueceu o mutex.
func SomarRapido(n int) {
	total += n
}

O teste abaixo exercita apenas Somar:

func TestSomar(t *testing.T) {
	total = 0
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			Somar(1)
		}()
	}
	wg.Wait()
	if total != 10 {
		t.Fatalf("total = %d, queria 10", total)
	}
}

Com go test -race -v, a saída foi:

=== RUN   TestSomar
--- PASS: TestSomar (0.00s)
PASS
ok  	race-aula/caminho-nao-testado	1.184s

Até aqui tudo parece certo. Depois de adicionar um segundo teste que chama SomarRapido em paralelo, com a mesma estrutura, go test -race -v passa a relatar a corrida e falha:

==================
WARNING: DATA RACE
Read at 0x000102c9c270 by goroutine 14:
  race-aula/caminho-nao-testado.SomarRapido()
      .../caminho-nao-testado/main.go:22 +0x6c
...
testing.go:1867: race detected during execution of test
--- FAIL: TestSomarRapido (0.00s)
FAIL
FAIL	race-aula/caminho-nao-testado	0.195s

O exit status do go test foi 1. Sem -race, o mesmo go test -v passa para os dois testes, porque a corrida existe mas não derruba o resultado daquela execução. O mesmo código recebeu três respostas diferentes: verde sem o detector, verde com o detector enquanto o caminho inseguro não rodava, e vermelho com o detector quando ele rodou.

A documentação do detector diz, em tradução livre, que ele só encontra corridas que acontecem durante a execução e, por isso, não consegue encontrar corridas em caminhos de código que não foram executados. A mesma página sugere rodar um binário compilado com -race sob uma carga realista para encontrar mais corridas. Isso aponta para duas coisas: cobertura de testes e carga parecida com a de produção. Um teste verde com o detector ligado é uma informação sobre o que rodou, e nada além disso.

Fluxo do detector: sem acesso concorrente, o silêncio é enganoso; com acesso concorrente sem trava, sai WARNING; com trava, o silêncio é correto.
figura 4 · Quando o detector de corrida acusa e quando fica em silêncio

O diagrama resume os dois caminhos de silêncio. O silêncio correto acontece quando os acessos concorrentes estão ordenados por trava ou canal. O silêncio enganoso acontece quando a corrida existe no código, mas nenhum dos acessos que a provocam rodou. Nos dois casos o relatório fica vazio, e só o primeiro significa que os acessos estavam certos.

Pergunta de entrevistaO race detector garante que o código não tem corrida?

Não. O detector, ativado com -race, marca acessos concorrentes à mesma memória, com pelo menos uma escrita, sem sincronização. Ele só enxerga os caminhos que a execução realmente percorreu. Por isso uso -race nos testes e com carga, mas não considero a ausência de alertas como prova. Ele também não pega race condition sem data race, como uma reserva que verifica e depois grava sem atomicidade.

Parte 6: Prática e consolidação

11. Exercícios práticos graduados

Tente resolver cada exercício antes de abrir o gabarito. Todos os gabaritos foram rodados nesta revisão.

Exercício 1: achar as corridas com o detector

Objetivo: o programa abaixo tem um painel de visitas que é atualizado por 400 goroutines. Rode com go run -race e identifique as linhas que aparecem no relatório. Depois corrija o código sem mudar a assinatura de Registrar.

type Painel struct {
	visitas int
	ultimo  string
}

func (p *Painel) Registrar(caminho string) {
	p.visitas++
	p.ultimo = caminho
}

O detector reportou três corridas nesta versão, com Found 3 data race(s) e exit status 66. As linhas envolvidas são a de p.visitas++ e a de p.ultimo = caminho.

Solução de referência:

type Painel struct {
	mu      sync.Mutex
	visitas int
	ultimo  string
}

func (p *Painel) Registrar(caminho string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.visitas++
	p.ultimo = caminho
}

Com a correção, a saída com -race foi visitas: 400, ultimo: /checkout e exit status 0. O valor de ultimo depende da ordem das goroutines, então pode ser qualquer um dos quatro caminhos. Aqui o que conta é a ausência de corrida, e o valor final fica em segundo plano.

Exercício 2: duas compras na última unidade

Objetivo: dois compradores tentam levar a última unidade de um estoque. Cada chamada a Disponivel e a Vender usa o mutex, e ainda assim o estoque pode ficar negativo. Explique por que o detector não reclama, e corrija.

type Estoque struct {
	mu       sync.Mutex
	unidades int
}

func (e *Estoque) Disponivel() bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.unidades > 0
}

func (e *Estoque) Vender() {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.unidades--
}

No main, e := &Estoque{unidades: 1} e duas goroutines, Carla e Diego, executam if e.Disponivel() { time.Sleep(10 * time.Millisecond); e.Vender() }.

Em três execuções sem -race, as duas compras aconteceram nas três, e o estoque terminou em -1. Com -race, o detector não emitiu nenhum aviso. Cada acesso ao campo unidades estava protegido, e a falha está entre a checagem e a venda.

Solução de referência:

// Comprar checa e vende dentro da mesma trava.
func (e *Estoque) Comprar() bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	if e.unidades <= 0 {
		return false
	}
	time.Sleep(10 * time.Millisecond) // só alarga a janela, como no exemplo da aula
	e.unidades--
	return true
}

Com essa versão, a saída com -race foi:

Carla comprou a última unidade
Diego encontrou esgotado
unidades restantes: 0

Exercício 3: fazer o teste expor o caminho esquecido

Objetivo: a partir do código de Somar e SomarRapido da seção 10, escreva um teste que faça o go test -race acusar a corrida em SomarRapido, sem alterar o código de produção.

Solução de referência:

func TestSomarRapido(t *testing.T) {
	total = 0
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			SomarRapido(1)
		}()
	}
	wg.Wait()
}

Com go test -race, esse teste falha com race detected during execution of test e exit status 1. Sem -race, ele passa. A correção do próprio SomarRapido é a mesma da seção 7: adicionar mu.Lock() e mu.Unlock() ao redor de total += n.

12. Glossário técnico

  • Race condition: situação em que o resultado de um programa depende da ordem ou do tempo relativo de eventos concorrentes, e alguma dessas ordens produz um resultado errado segundo a regra de negócio.
  • Data race: acesso concorrente à mesma posição de memória, sem sincronização entre os acessos, com pelo menos uma escrita.
  • Janela de corrida: o intervalo de tempo em que outra goroutine pode agir entre dois passos que deveriam ser indivisíveis.
  • Intercalação: uma das ordens possíveis em que as operações de duas ou mais goroutines podem se alternar.
  • Atualização perdida: efeito de uma escrita que sobrescreve outra feita a partir de um valor antigo, apagando a alteração anterior.
  • Seção crítica: trecho de código que só pode ser executado por uma goroutine por vez, protegido por uma trava.
  • Mutex: trava de exclusão mútua. Lock bloqueia até que outra goroutine chame Unlock.
  • Canal: mecanismo de comunicação entre goroutines. Sem buffer, quem envia espera quem recebe.
  • Dona do estado: goroutine que é a única a ler e escrever uma variável, enquanto as outras enviam pedidos para ela.
  • Sincronização: mecanismo que estabelece uma ordem entre dois acessos, como um mutex ou um canal.
  • Detector de corrida: ferramenta ativada pela flag -race, derivada do ThreadSanitizer, que observa os acessos de memória durante a execução e relata data races que acontecem nela.
  • Happens-before: relação de ordem que a sincronização estabelece entre dois acessos. Se um acesso acontece antes de outro por essa relação, o segundo enxerga o que o primeiro escreveu.

13. O que não foi confirmado

As saídas e os números vêm de um MacBook Pro com Apple M1 Max, dez núcleos lógicos, macOS (Darwin 27.0.0) e Go 1.27.2 em darwin/arm64. Linux e outras versões do Go não foram executados nesta revisão, e o comportamento do detector nessas plataformas não foi conferido.

As perdas do contador variam entre execuções. A faixa observada, de 0 a 885 736 em oito execuções, não é uma estatística. Não medi a probabilidade de corrida para esse programa, e nenhuma frequência é afirmada aqui.

A espera de 10 milissegundos nas demos de reserva e de estoque serve para tornar o efeito estável. O comportamento desses programas sem a espera não foi medido.

O custo do detector, de 5 a 10 vezes mais memória e de 2 a 20 vezes mais tempo, vem da documentação oficial. Não medi esse custo no programa desta aula.

A frase sobre o modelo de memória do Go, de que só há ordem entre acessos ligados por uma sincronização, é uma leitura da documentação do detector e da prática. O texto completo do modelo de memória (go.dev/ref/mem) foi citado pela documentação, mas não foi lido nesta revisão.

A referência ao livro de Donovan e Kernighan aparece nas fontes apenas como indicação de leitura. O capítulo sobre variáveis compartilhadas não foi conferido nesta revisão.

14. Fontes e leituras recomendadas

Consultadas em 3 de novembro de 2026.

  • The Go Authors. “Data Race Detector”. Documentação oficial. https://go.dev/doc/articles/race_detector. A página não mostra data de publicação ou de atualização.
  • Vyukov, Dmitry; Gerrand, Andrew. “Introducing the Go Race Detector”. The Go Blog, 26 de junho de 2013. https://go.dev/blog/race-detector.
  • The Go Authors. Especificação da linguagem, seção sobre canais, consultada na cópia local da toolchain go1.27.2 (doc/go_spec.html).
  • The Go Authors. go help build e go doc sync.Mutex, consultados na toolchain local go1.27.2 (darwin/arm64).
  • The Go Authors. “The Go Memory Model”. https://go.dev/ref/mem. Citada pela documentação do detector e não lida nesta revisão.
  • Donovan, Alan A. A.; Kernighan, Brian W. The Go Programming Language. Addison-Wesley, 2015. Capítulo sobre variáveis compartilhadas.

15. Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?