ЗАМЕР
bench/gocontext/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/context
Запись прогона
У этого замера записи прогона нет — только скрипт.
Скрипт
221 строк//go:build ignore
// Context изнутри: дерево отмены, утечка без cancel и цена Value.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Три утверждения про контекст повторяют все, и все три
// проверяемы:
//
// 1. «не вызвал cancel — утечка». Утечка чего именно? Ответ: горутины у
// WithTimeout и записи в списке потомков у родителя. Считается.
// 2. «контекст — дерево, отмена идёт вниз». Проверяется тем, что у родителя
// после отмены потомка Err() остаётся nil.
// 3. «Value дешёвый». Дёшево класть, дорого доставать, и цена зависит от
// глубины.
//
// ЗАПУСК:
//
// go run bench/gocontext/internals.go
package main
import (
"context"
"fmt"
"runtime"
"testing"
"time"
)
type ctxKey string
const userKey ctxKey = "user"
var (
sink any
sinkI int
)
// ------------------------------------------------- блок 1: утечка без cancel
// leak показывает, чем именно оборачивается забытый cancel.
//
// ПОЧЕМУ ЗДЕСЬ ПАМЯТЬ, А НЕ ГОРУТИНЫ. Первая редакция считала
// runtime.NumGoroutine() — и получила НОЛЬ прибавившихся горутин в обоих
// случаях. Утверждение «забытый cancel течёт горутинами» замер не подтвердил,
// и его пришлось убрать: WithTimeout заводит таймер рантайма, а не горутину.
//
// Течёт при этом настоящее и измеримое: пока cancel не вызван, потомок
// остаётся в СПИСКЕ ПОТОМКОВ родителя, и ни он сам, ни то, на что он
// ссылается, собрано быть не может. Родитель в реальном коде живёт долго —
// это контекст запроса или сервиса, — поэтому список растёт вместе с числом
// забытых cancel.
func leak() {
fmt.Println("ЧТО ИМЕННО ТЕЧЁТ, КОГДА ЗАБЫЛИ CANCEL")
fmt.Println("─────────────────────────────────────")
const n = 10000
// Долгоживущий родитель — как контекст сервиса или запроса.
parent, cancelParent := context.WithCancel(context.Background())
defer cancelParent()
var m0, m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m0)
// Десять тысяч потомков, у которых cancel НЕ вызван.
for i := 0; i < n; i++ {
_, cancel := context.WithCancel(parent)
_ = cancel // намеренно не вызываем: это и есть предмет опыта
}
runtime.GC()
runtime.ReadMemStats(&m1)
leaked := (m1.HeapAlloc - m0.HeapAlloc) / n
// Столько же потомков, но с честным cancel.
for i := 0; i < n; i++ {
_, cancel := context.WithCancel(parent)
cancel()
}
runtime.GC()
runtime.ReadMemStats(&m2)
kept := int64(m2.HeapAlloc-m1.HeapAlloc) / n
fmt.Printf(" %d потомков без cancel осталось в куче: %d байт на потомка\n", n, leaked)
fmt.Printf(" %d потомков с cancel осталось в куче: %d байт на потомка\n", n, kept)
fmt.Println()
fmt.Println(" Вот это и есть «утечка контекста», и она не про горутины:")
fmt.Println(" WithTimeout заводит таймер рантайма, а не горутину. Течёт")
fmt.Println(" другое — пока cancel не вызван, потомок остаётся в СПИСКЕ")
fmt.Println(" ПОТОМКОВ родителя, и ни он, ни то, на что он ссылается,")
fmt.Println(" собрано быть не может.")
fmt.Println()
fmt.Println(" Родитель в реальном коде живёт долго — это контекст сервиса")
fmt.Println(" или соединения, — поэтому список растёт вместе с числом")
fmt.Println(" забытых cancel. Отсюда идиома defer cancel(): он вызывается")
fmt.Println(" ВСЕГДА, даже когда работа закончилась успешно, потому что он")
fmt.Println(" не «отменяет результат», а вычёркивает потомка из списка.")
fmt.Println()
}
// ------------------------------------------------- блок 2: дерево
func tree() {
fmt.Println("ОТМЕНА ИДЁТ ВНИЗ, А НЕ ВВЕРХ И НЕ ВБОК")
fmt.Println("──────────────────────────────────────")
root, cancelRoot := context.WithCancel(context.Background())
a, cancelA := context.WithCancel(root)
b, cancelB := context.WithCancel(root)
defer cancelRoot()
defer cancelA()
defer cancelB()
cancelA()
fmt.Printf(" отменили A: A=%v B=%v корень=%v\n", errText(a.Err()), errText(b.Err()), errText(root.Err()))
cancelRoot()
// Небольшая пауза: отмена распространяется по дереву асинхронно.
time.Sleep(10 * time.Millisecond)
fmt.Printf(" отменили корень: A=%v B=%v корень=%v\n", errText(a.Err()), errText(b.Err()), errText(root.Err()))
fmt.Println()
fmt.Println(" Первая строка: отмена потомка не трогает ни родителя, ни")
fmt.Println(" соседа. Вторая: отмена родителя доходит до ВСЕХ потомков.")
fmt.Println(" Это и значит «контекст — дерево»: связь односторонняя.")
fmt.Println()
fmt.Println(" Отсюда практическое: отменить чужую работу можно, только")
fmt.Println(" держа её контекст, — и поэтому контекст передают первым")
fmt.Println(" аргументом, а не прячут в структуру.")
fmt.Println()
}
func errText(err error) string {
if err == nil {
return "nil"
}
return err.Error()
}
// ------------------------------------------------- блок 3: цена Value
func buildChain(n int) context.Context {
ctx := context.WithValue(context.Background(), userKey, "roman")
for i := 0; i < n; i++ {
ctx = context.WithValue(ctx, ctxKey(fmt.Sprintf("k%d", i)), i)
}
return ctx
}
func valueCost() {
fmt.Println("ЦЕНА VALUE ПО ГЛУБИНЕ ВЛОЖЕНИЯ")
fmt.Println("──────────────────────────────")
put := testing.AllocsPerRun(1000, func() {
sink = context.WithValue(context.Background(), userKey, "roman")
})
fmt.Printf(" положить одно значение выделений: %.0f\n", put)
fmt.Println()
fmt.Printf(" %-22s %12s\n", "слоёв над значением", "Value")
var base float64
for _, n := range []int{0, 5, 10, 20} {
ctx := buildChain(n)
v := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = ctx.Value(userKey)
}
})
if base == 0 {
base = v
}
fmt.Printf(" %-22d %9.2f нс ×%.1f\n", n, v, v/base)
}
fmt.Println()
// Для сравнения — то, чем контекст НЕ является: обычное поле структуры.
type req struct{ user string }
r := &req{user: "roman"}
field := best(func(b *testing.B) {
for i := 0; i < b.N; i++ {
sinkI += len(r.user)
}
})
fmt.Printf(" поле структуры, для сравнения %9.2f нс\n", field)
fmt.Println()
fmt.Println(" Класть в контекст дёшево — одно выделение. Дорого ДОСТАВАТЬ:")
fmt.Println(" Value идёт вверх по цепочке родителей слой за слоем, и цена")
fmt.Println(" растёт вместе с глубиной, как у errors.Is по цепочке обёрток.")
fmt.Println()
fmt.Println(" Последняя строка — мера того, насколько это не замена полю.")
fmt.Println(" Контекст переносит то, что относится к ЗАПРОСУ и проходит")
fmt.Println(" через границы пакетов: идентификатор трассировки, дедлайн,")
fmt.Println(" данные аутентификации. Параметры функции в него не кладут.")
fmt.Println()
}
// ------------------------------------------------------------ утилиты
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func best(f func(*testing.B)) float64 {
out := 0.0
for r := 0; r < rounds; r++ {
v := nsPerOp(testing.Benchmark(f))
if out == 0 || v < out {
out = v
}
}
return out
}
func main() {
fmt.Printf("%s %s/%s | context изнутри\n\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
leak()
tree()
valueCost()
}