ЗАМЕР
bench/gorace/options.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/data-races
- Как запустить
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
Запись прогона
Замеры для урока «Гонки данных»
| Файл | Что делает |
|---|---|
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.
Скрипт
79 строк//go:build ignore
// Настройки и требования детектора гонок: код выхода и cgo.
//
// ЗАЧЕМ. Урок называет код выхода 66 и говорит, на каких платформах и с какими
// условиями работает -race. Оба утверждения проверяются здесь, а не
// пересказываются:
//
// 1. 66 — значение по умолчанию; переменная окружения GORACE=exitcode=N его
// меняет.
// 2. На linux -race требует cgo: сборка с CGO_ENABLED=0 отказывается.
//
// ЗАПУСК:
//
// go run bench/gorace/options.go
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
func main() {
_, file, _, _ := runtime.Caller(0)
dir := filepath.Join(filepath.Dir(file), "racy", "seq")
fmt.Printf("%s %s/%s | настройки детектора гонок\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
bin := filepath.Join(os.TempDir(), "gorace-options-seq")
build := exec.Command("go", "build", "-race", "-o", bin, ".")
build.Dir = dir
if out, err := build.CombinedOutput(); err != nil {
panic(fmt.Sprintf("%v\n%s", err, out))
}
fmt.Println()
fmt.Println("1) КОД ВЫХОДА: ПО УМОЛЧАНИЮ И ЧЕРЕЗ GORACE")
fmt.Println("──────────────────────────────────────────")
for _, env := range []string{"", "GORACE=exitcode=13"} {
cmd := exec.Command(bin)
cmd.Env = os.Environ()
if env != "" {
cmd.Env = append(cmd.Env, env)
}
var buf bytes.Buffer
cmd.Stdout, cmd.Stderr = &buf, &buf
err := cmd.Run()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
}
label := env
if label == "" {
label = "без GORACE"
}
fmt.Printf(" %-20s отчётов %d, код выхода %d\n", label+":",
strings.Count(buf.String(), "WARNING: DATA RACE"), code)
}
fmt.Println()
fmt.Println("2) -race БЕЗ cgo")
fmt.Println("────────────────")
nocgo := exec.Command("go", "build", "-race", "-o", os.DevNull, ".")
nocgo.Dir = dir
nocgo.Env = append(os.Environ(), "CGO_ENABLED=0")
out, err := nocgo.CombinedOutput()
fmt.Printf(" CGO_ENABLED=0 go build -race: %s", out)
if ee, ok := err.(*exec.ExitError); ok {
fmt.Printf(" код выхода go build: %d\n", ee.ExitCode())
}
fmt.Println(" На linux детектор требует cgo, то есть и C-компилятор на")
fmt.Println(" машине сборки. Исключение — macOS: по исходнику cmd/go там")
fmt.Println(" -race собирается и без cgo.")
}