Deep Engineering

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("  записью и чтением нет ни одного события синхронизации.")
}