MEASUREMENT
bench/gorace/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/data-races
- How to run it
go run bench/gorace/internals.go go run bench/gorace/detector.go # собирает racy/* сам go run bench/gorace/limits.go # тоже go run bench/gorace/options.go go run bench/gorace/practice.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Гонки данных»
| Файл | Что делает |
|---|---|
internals.go |
без -race: разорванные чтения структуры из двух слов и цикл по флагу без синхронизации |
detector.go |
собирает программы из racy/ с -race и без: отчёт детектора, гонка на невыполненном пути, состязание без гонки данных, цена режима -race по времени и памяти |
racy/counter |
гонка в чистом виде: две горутины пишут одну переменную |
racy/branch |
гонка только на редком пути, включаемом аргументом |
racy/checkact |
проверка и действие под разными захватами одного мьютекса |
racy/workload |
нагрузка без гонок для замера цены детектора |
limits.go |
границы детектора сверх detector.go: гонка без одновременности, выполненная гонка без отчёта, окно без runtime.Gosched(), go test -race на тесте с прошедшими проверками |
options.go |
код выхода по умолчанию и через GORACE=exitcode=N; сборка -race без cgo |
racy/seq |
запись и чтение, разнесённые на 100 мс, без синхронизации |
racy/missed |
гонка, которую в этом чередовании случайно упорядочил замок |
racy/testpkg |
тест с гонкой, все проверки которого проходят |
practice.go |
источник ответов двух практических задач урока |
go run bench/gorace/internals.go
go run bench/gorace/detector.go # собирает racy/* сам
go run bench/gorace/limits.go # тоже
go run bench/gorace/options.go
go run bench/gorace/practice.go
Записи прогонов — в runs/, go1.24.7 linux/amd64, GOMAXPROCS=2.
Что здесь важно прочитать правильно
В internals.go гонки намеренные. Это программа про эффект гонки, а не
про её обнаружение; запускать её с -race незачем.
Доли разорванных чтений шумят. От круга к кругу — от половины до трёх четвертей прочитанных пар. Устойчиво только то, что их не ноль. Поэтому в практической задаче печатается факт, а не доля.
Цикл for !done {} на go1.24.7 завершился. Это наблюдение, а не
гарантия: модель памяти про этот самый цикл говорит, что он может не
завершиться никогда. Урок обязан называть это так.
Что получилось
Детектор находит гонку, которая не испортила результат. racy/counter без
-race печатает правильные n = 2000; с -race — отчёт WARNING: DATA RACE
и код выхода 66.
Детектор не находит гонку, которая не выполнилась. racy/branch с -race
на обычном пути — код выхода 0 и ни одного отчёта; тот же бинарник с
аргументом rare — отчёт с указанием на main.go:24.
Детектор не находит состязание без гонки данных. racy/checkact: баланс
ушёл в минус в 936 попытках из 1000, отчётов ноль. Каждое обращение под
замком; ошибка — в том, что проверка и списание разнесены по двум захватам.
Число попыток зависит от явного runtime.Gosched() между проверкой и
действием — он поставлен, чтобы окно было видно в каждом прогоне.
Цена режима -race. Та же нагрузка, три серии подряд: по памяти
×4,8–5,0, по времени ×7,6–12,0. Время гуляет заметно, память — нет; поэтому
практическая задача спрашивает про память (×4,85 в её прогоне).
Одновременность детектору не нужна. racy/seq: запись и чтение разнесены
на 100 мс, синхронизации между ними нет — отчёт в 5 запусках из 5, в том числе
при GOMAXPROCS=1.
Гоночная программа может дать исполнение без гонки. racy/missed: запись
и чтение мимо замка, но в этих запусках Unlock в горутине случается раньше
Lock в main, обращения оказываются упорядочены, и в этом исполнении гонки
данных нет — 0 отчётов за 5 запусков. Детектор проверяет исполнение, а не все
чередования, которые программа допускает.
Без runtime.Gosched() окно остаётся. racy/checkact nogosched: 44, 20 и
2 ухода в минус из 1000 при нуле отчётов. С Gosched в том же прогоне — 943
(в записи detector.txt — 936: разброс между прогонами).
go test -race падает на гонке, а не на проверке. racy/testpkg: проверка
теста не срабатывает, пакет testing пишет
race detected during execution of test, код выхода go test — 1.
66 — значение по умолчанию. options.go: тот же отчёт с
GORACE=exitcode=13 завершается с кодом 13.
На linux -race требует cgo. CGO_ENABLED=0 go build -race отказывается:
-race requires cgo. По исходнику cmd/go go1.24.7 исключение — macOS.
Script
119 lines//go:build ignore
// Гонка данных изнутри: что именно ломается, когда синхронизации нет.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. «Гонка — это когда счётчик теряет инкременты» — так
// тему обычно и пересказывают, и из этого вырастает опасная мысль: если
// результат не важен до единицы, гонка безобидна. Здесь две вещи, которые эту
// мысль опровергают, и обе — на программе БЕЗ -race: детектор тут не нужен,
// нужен сам эффект.
//
// 1. РАЗОРВАННОЕ ЗНАЧЕНИЕ. Структура из двух слов, писатель всегда кладёт в
// оба поля одно число. Читатель без синхронизации видит пары, которых
// никто никогда не записывал. Модель памяти Go называет это прямо: гонка
// на многословных значениях может дать «значение, не соответствующее ни
// одной записи».
// 2. ФЛАГ БЕЗ СИНХРОНИЗАЦИИ. Цикл `for !done {}` из модели памяти, о котором
// там сказано, что он может не завершиться никогда. Печатается, что
// произошло на этой версии Go, — и это НЕ обещание: модель памяти
// гарантирует обратное, то есть ничего.
//
// Программа содержит гонки намеренно. С -race её запускать незачем: отчёт
// будет, а наблюдать здесь нужно сам эффект.
//
// ЗАПУСК:
//
// go run bench/gorace/internals.go
package main
import (
"fmt"
"runtime"
"strings"
"sync/atomic"
"time"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// ------------------------------------------------ 1. разорванное значение
type pair struct{ a, b int64 }
var shared pair
// tornRound — 200 мс писатель кладёт {i, i}, читатель копирует структуру и
// проверяет, совпадают ли поля. Возвращает число разорванных копий и всех.
func tornRound() (torn, reads int) {
var stop atomic.Bool
ready := make(chan struct{})
go func() {
close(ready)
for i := int64(1); !stop.Load(); {
for j := 0; j < 1024; j++ {
shared = pair{i, i}
i++
}
}
}()
<-ready
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
for j := 0; j < 1024; j++ {
x := shared
reads++
if x.a != x.b {
torn++
}
}
}
stop.Store(true)
return torn, reads
}
// ------------------------------------------------- 2. флаг без синхронизации
var done bool
func main() {
fmt.Printf("%s %s/%s, GOMAXPROCS=%d | гонка данных изнутри\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
show("1) РАЗОРВАННОЕ ЗНАЧЕНИЕ: struct{ a, b int64 }")
fmt.Println(" писатель всегда кладёт {i, i}; читатель копирует без замка")
fmt.Printf(" %-6s %16s %16s %8s\n", "круг", "прочитано", "a != b", "доля")
for r := 1; r <= 5; r++ {
torn, reads := tornRound()
fmt.Printf(" %-6d %16d %16d %7.1f%%\n", r, reads, torn, 100*float64(torn)/float64(reads))
}
fmt.Println()
fmt.Println(" Ни одной пары с разными полями никто не записывал. Копия")
fmt.Println(" структуры из двух слов — не одно неделимое чтение, и писатель")
fmt.Println(" успевает вклиниться посередине. Доля меняется от круга к кругу")
fmt.Println(" и от машины к машине; устойчиво только то, что она не ноль.")
show("2) ФЛАГ БЕЗ СИНХРОНИЗАЦИИ: for !done {}")
seen := make(chan struct{})
go func() {
for !done {
}
close(seen)
}()
time.Sleep(10 * time.Millisecond)
done = true
select {
case <-seen:
fmt.Println(" на этой версии цикл увидел запись и завершился")
case <-time.After(time.Second):
fmt.Println(" цикл не увидел запись за 1 с")
}
fmt.Println()
fmt.Println(" Это наблюдение, а не гарантия. Модель памяти про этот самый")
fmt.Println(" цикл говорит, что он может не завершиться никогда: между")
fmt.Println(" записью и чтением нет ни одного события синхронизации.")
}