Deep Engineering

ЗАМЕР

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.")
}