MEASUREMENT
bench/gosync/practice.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/sync-atomic
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
This measurement has no recorded run — only the script.
Script
197 lines//go:build ignore
// Две задачи урока про sync и atomic: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Три вещи, которые путают: инкремент без
// синхронизации теряет обновления; sync.Once выполняет функцию ровно один раз
// даже при гонке; а копия структуры с мьютексом — это уже другой мьютекс.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз мьютекс дороже атомарной
// операции на ОДНОМ И ТОМ ЖЕ счётчике без конкуренции. Именно без
// конкуренции: это тот случай, о котором и спорят, и в нём разница видна
// чище всего.
//
// ЗАПУСК:
//
// go run bench/gosync/practice.go
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"testing"
)
// ---------------------------------------------------------------- задача 1
const (
workers = 8
perGo = 10000
)
// unsynced — инкремент обычной переменной из восьми горутин. Печатается не
// само значение (оно недетерминированное), а факт потери: меньше ли оно
// ожидаемого.
//
// ПОЧЕМУ ЗДЕСЬ ЯВНАЯ УСТУПКА ПРОЦЕССОРА. Первая редакция писала просто `n++`
// в цикле — и один из прогонов дал РОВНО 80 000, то есть «потери нет». Это не
// опровержение гонки, а её главное свойство: она проявляется не всегда.
// Задача, ответ которой меняется от запуска к запуску, негодна, поэтому
// чтение и запись здесь разведены явно. Так гонка становится наблюдаемой
// каждый раз — а в обычном `n++` она остаётся ровно такой же ошибкой,
// просто менее заметной.
func unsynced() bool {
n := 0
var wg sync.WaitGroup
wg.Add(workers)
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := 0; i < perGo; i++ {
v := n
runtime.Gosched()
n = v + 1
}
}()
}
wg.Wait()
return n < workers*perGo
}
// syncedAtomic — тот же счётчик атомарно.
func syncedAtomic() int64 {
var n atomic.Int64
var wg sync.WaitGroup
wg.Add(workers)
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
for i := 0; i < perGo; i++ {
n.Add(1)
}
}()
}
wg.Wait()
return n.Load()
}
// onceCount — сколько раз выполнится функция под sync.Once, если её дёрнуть
// из восьми горутин сразу.
func onceCount() int64 {
var once sync.Once
var calls atomic.Int64
var wg sync.WaitGroup
wg.Add(workers)
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
once.Do(func() { calls.Add(1) })
}()
}
wg.Wait()
return calls.Load()
}
// counter — структура с мьютексом внутри. Копия такой структуры получает
// СВОЙ мьютекс и свои данные.
type counter struct {
mu sync.Mutex
n int
}
func (c *counter) inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}
// copiedStruct показывает, что копия — это другой объект.
func copiedStruct() (int, int) {
orig := &counter{}
orig.inc()
cp := *orig //nolint:govet // копирование мьютекса здесь и есть предмет опыта
cp.inc()
return orig.n, cp.n
}
func task1() {
fmt.Println(unsynced())
fmt.Println(syncedAtomic())
fmt.Println(onceCount())
a, b := copiedStruct()
fmt.Println(a, b)
}
// ---------------------------------------------------------------- задача 2
// ПОЧЕМУ БЕЗ КОНКУРЕНЦИИ. Спор «мьютекс или atomic» почти всегда идёт про
// счётчик, к которому ходят из многих горутин. Но чтобы увидеть разницу самих
// ПРИМИТИВОВ, конкуренцию надо убрать: иначе замер покажет цену ожидания, а
// она у обоих своя и зависит от числа горутин.
//
// Замер с конкуренцией сделан отдельно, в internals.go, — и там картина
// другая, что и есть главный вывод урока.
var (
mu sync.Mutex
plain int64
atomCnt atomic.Int64
)
func withMutex(b *testing.B) {
for i := 0; i < b.N; i++ {
mu.Lock()
plain++
mu.Unlock()
}
}
func withAtomic(b *testing.B) {
for i := 0; i < b.N; i++ {
atomCnt.Add(1)
}
}
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func main() {
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
task1()
fmt.Println()
m, a := 0.0, 0.0
for r := 0; r < rounds; r++ {
// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
mm := nsPerOp(testing.Benchmark(withMutex))
aa := nsPerOp(testing.Benchmark(withAtomic))
if m == 0 || mm < m {
m = mm
}
if a == 0 || aa < a {
a = aa
}
}
fmt.Println("ЗАДАЧА 2 — во сколько раз мьютекс дороже atomic без конкуренции")
fmt.Println()
fmt.Printf(" mu.Lock/Unlock + инкремент %9.2f нс\n", m)
fmt.Printf(" atomic.Int64.Add %9.2f нс\n", a)
fmt.Printf(" кратность %9.2f\n", m/a)
fmt.Printf(" она же округлённо %9.1f\n", m/a)
fmt.Println()
fmt.Printf(" Лучший из %d чередующихся кругов. %s %s/%s, GOMAXPROCS=%d\n",
rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
fmt.Println()
fmt.Println(" Работа одинаковая — прибавить единицу. Отличается только")
fmt.Println(" примитив. Конкуренции здесь НЕТ: это цена самих операций,")
fmt.Println(" а не цена ожидания. С конкуренцией картина другая —")
fmt.Println(" см. bench/gosync/internals.go.")
}