MEASUREMENT
bench/gopattern/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/concurrency-patterns
- How to run it
go run bench/gopattern/internals.go /usr/local/go1.25.1/bin/go run bench/gopattern/internals.go go run bench/gopattern/practice.go go run bench/gopattern/cancel.go go run bench/gopattern/semaphore.go (cd bench/gopattern/cases && go run ./pipeline)
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 |
тринадцать наблюдений: пул воркеров на задаче для процессора (1, 2, 4, 8 воркеров при GOMAXPROCS=2 и 1, 8 при GOMAXPROCS=1, со столбцом «ядер занято») и на задаче со сном (1–16 воркеров); порядок результатов и индекс; конвейер gen → sq; забытый close без таймеров и с таймером; wg.Wait() до чтения результатов, в том числе с буфером; fan-in с close после wg.Wait() и без него; семафор на буферизованном канале; потребитель, ушедший раньше, без context и с ним; остановка по первой ошибке (свой минимальный аналог errgroup.WithContext на WaitGroup, Once и context.WithCancelCause); sync.WaitGroup.Go |
cases/forgotclose/main.go |
конвейер, где sq забыла close; намеренно падает (раздел 5) |
cases/waitbeforeread/main.go |
пул, где wg.Wait() стоит до чтения результатов; намеренно падает (раздел 6) |
cases/waitbuffered/main.go |
та же ошибка с буфером результатов на 6; число задач — аргумент; на 7 падает (раздел 6) |
cases/earlyclose/main.go |
слияние, где close(out) стоит без wg.Wait(); намеренно падает (раздел 8) |
cases/wggo/main.go |
sync.WaitGroup.Go; на go1.24.7 не собирается (раздел 13) |
cases/go.mod |
свой модуль для подопытных программ, go 1.24 |
cancel.go |
отмена стадии конвейера на четырёх сценариях (обычный поток, закрытый вход, отмена при открытом пустом и nil-входе, отмена при заблокированном выходе) для двух версий стадии: range по входу с select только на отправке и select в обеих точках ожидания; завершение определяется закрытием выхода за 500 мс, а не счётом горутин |
semaphore.go |
семафор на канале: место внутри уже запущенной горутины, место до go, ожидание места с ctx.Done(); горутины считаются своим счётчиком живых обработчиков |
cases/pipeline/main.go |
полный короткий пример урока: конвейер из двух стадий с отменой; файл без кириллицы — его показывают обе локали |
practice.go |
источник ответов двух практических задач урока: утечка двух горутин и семафор ёмкостью 2; кратность 8 воркеров к одному на задаче для процессора |
go run bench/gopattern/internals.go
/usr/local/go1.25.1/bin/go run bench/gopattern/internals.go
go run bench/gopattern/practice.go
go run bench/gopattern/cancel.go
go run bench/gopattern/semaphore.go
(cd bench/gopattern/cases && go run ./pipeline)
internals.go собирает и запускает cases/* тем же go, которым запущен сам
(runtime.GOROOT()/bin/go), с GOTOOLCHAIN=local. Путь к cases в выводе
заменён на <cases>, номера горутин — на <N>, смещения +0x… в стеке
убраны.
Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра.
runs/internals-go1.25.txt — тот же internals.go под go1.25.1: отличается
раздел 13 (WaitGroup.Go есть, go vet предупреждает о версии в go.mod) и
миллисекунды в разделах 1 и 2. cancel.go, semaphore.go и
cases/pipeline на go1.25.1 дают тот же вывод, кроме строки версии
(проверено diff), поэтому отдельных записей для них нет; cancel.go и
semaphore.go дали одинаковый вывод в трёх запусках подряд.
Итоговая фраза раздела 9 internals.go — «Семафор ограничивает работу, а не
число горутин: все 12 созданы» — верна для показанной там расстановки (место
занимается внутри уже запущенной горутины). При месте, занятом до go, живых
обработчиков не больше ёмкости — semaphore.go, раздел 2.
Функция sqCtx в internals.go читает вход через range и слушает отмену
только на отправке; в связке раздела 11 этого хватает, а на открытом пустом
входе — нет (cancel.go, раздел 1). Оставлена как есть, чтобы
runs/internals.txt соответствовал коду.
Машину делят другие процессы. Времена — лучший из N кругов, конфигурации в
разделе 1 и в practice.go прогоняются вперемешку. Столбец
«ядер занято» — процессорное время процесса (getrusage), делённое на
настенное: если соседи займут ядро, это видно в нём. Кратности — наблюдение
этой конфигурации (go1.24.7/go1.25.1, linux/amd64, 2 ядра), а не константа:
на другой машине, задаче и версии их нужно мерить заново.
Что получилось
На задаче для процессора больше GOMAXPROCS воркеров не ускоряет. При
GOMAXPROCS=2 два воркера — 2,0 к одному, четыре и восемь — 1,9. Что потолок
ставит именно GOMAXPROCS, видно по GOMAXPROCS=1: восемь воркеров — 1,0 при
«ядер занято» 1,0, хотя свободных ядер два (выше 2,0 этот столбец на двух ядрах
не поднимется при любом GOMAXPROCS).
На задаче со сном ускорение растёт с числом воркеров. 16 задач по 20 мс: 324 мс у одного воркера, 41 мс у восьми (8,0), 20 мс у шестнадцати (16,0).
Результаты приходят в порядке готовности. Задачи a b c d по 80, 20, 60,
40 мс пришли как [b d c a] во всех 20 повторах этого прогона; по индексу —
[A B C D]. Порядок прихода не гарантирован: на загруженной машине он может
поменяться, поэтому строка «совпал … из 20» от запуска к запуску может
отличаться.
Забытый close роняет программу, только если спят все. cases/forgotclose:
fatal error: all goroutines are asleep - deadlock!, код выхода 2. Та же
стадия в программе с живым time.After: через 500 мс потребитель (горутина,
запущенная из main.main) всё ещё в range, падения нет, +1 горутина. Причина — ранние выходы checkdead в
runtime/proc.go (строки 6055–6058 и 6119–6124 на go1.24.7).
wg.Wait() до чтения результатов — взаимная блокировка. main в
[sync.WaitGroup.Wait], четыре горутины в [chan send]. С буфером на 6
программа проходит на 6 задачах и падает на 7.
Ранний close в слиянии — паника. cases/earlyclose: получено 0
значений, затем panic: send on closed channel в строке out <- v.
Семафор ограничивает работу, а не горутины. Ёмкость 3: одновременно работали 3, горутин было 12, 81 мс против 20 без семафора.
Потребитель, ушедший без context, оставляет горутины. +2, обе в
[chan send]; с select на ctx.Done() и defer cancel() — +0. Минимальная
группа с первой ошибкой прерывает долгую работу на 10 с и возвращает первую
ошибку, context.Cause отдаёт её же.
sync.WaitGroup.Go — с go1.25. На go1.24.7: wg.Go undefined. На
go1.25.1 программа печатает сумму, а go vet при go 1.24 в go.mod пишет
sync.Go requires go1.25 or later (module is go1.24).
Отмену стадия слышит только там, где её слушает. Стадия с range по входу
и select только на отправке на открытом пустом входе и на nil-входе после
cancel() выход за 500 мс не закрыла; стадия с select в обеих точках
ожидания закрыла его во всех четырёх сценариях (cancel.go).
Семафор ограничивает и горутины, если место занимать до go. Место
внутри горутины: обработчиков жило одновременно 12, работало 3. Место до go:
жило 3, работало 3. С ctx.Done() в ожидании места раздача остановилась после
трёх запусков с context canceled (semaphore.go).
Script
772 lines//go:build ignore
// Паттерны конкурентности в Go: пул воркеров, конвейер, fan-in, семафор,
// остановка по отмене — и что ломается, если пропустить одну строку.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Паттерны обычно пересказывают картинкой: «N
// горутин читают один канал», «стадии соединены каналами». На собеседовании
// спрашивают не картинку, а последствия: сколько воркеров нужно задаче на
// процессоре и сколько — задаче, которая ждёт; в каком порядке придут
// результаты; что будет, если стадия забудет close; куда деваются горутины,
// когда потребитель ушёл раньше времени. Каждое из этого здесь проверяется
// запуском.
//
// РАЗДЕЛЫ:
//
// 1. Пул воркеров на задаче для процессора: 1, 2, 4, 8 воркеров при двух
// ядрах — ускорение упирается в GOMAXPROCS.
// 2. Тот же пул на задаче со сном (ожидание диска, сети): ускорение растёт
// с числом воркеров.
// 3. Порядок результатов: приходят в порядке готовности, а не задач; индекс
// возвращает порядок.
// 4. Конвейер gen → sq → потребитель: close — сигнал «больше не будет».
// 5. Стадия забыла close: fatal error, если спят все (cases/forgotclose).
// 6. wg.Wait() до чтения результатов: тот же fatal error, видно, кто кого
// ждёт (cases/waitbeforeread); буфер маскирует ошибку, пока задач не
// больше ёмкости (cases/waitbuffered).
// 7. Стадия забыла close, но в программе есть таймер: никакого fatal error,
// потребитель просто висит.
// 8. Fan-in: слияние каналов, close(out) после wg.Wait — и что будет без
// Wait (cases/earlyclose).
// 9. Семафор на буферизованном канале: наибольшее число одновременно
// работающих равно ёмкости.
// 10. Потребитель ушёл раньше: конвейер без context теряет горутины.
// 11. Тот же конвейер с context: горутин после — столько же, сколько до.
// 12. Остановка по первой ошибке: минимальная группа на WaitGroup и
// context.WithCancelCause.
// 13. sync.WaitGroup.Go: есть ли он в этой версии (cases/wggo).
//
// Времена шумные: машину делят другие процессы. Поэтому каждое время — лучший
// из нескольких кругов. И миллисекунды, и кратности — наблюдение этой
// конфигурации, а не константа: на другой машине их нужно мерить заново.
//
// ЗАПУСК:
//
// go run bench/gopattern/internals.go
// /usr/local/go1.25.1/bin/go run bench/gopattern/internals.go
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// settle даёт вышедшим горутинам время действительно завершиться, прежде чем
// их считать: return из функции горутины и её исчезновение из счёта —
// не одно и то же мгновение.
func settle() int {
for i := 0; i < 20; i++ {
runtime.Gosched()
time.Sleep(5 * time.Millisecond)
}
return runtime.NumGoroutine()
}
func ms(d time.Duration) string {
return fmt.Sprintf("%d мс", (d+500*time.Microsecond)/time.Millisecond)
}
// ------------------------------------------------------------ пул воркеров
type result struct {
idx int
val uint64
}
// pool — классический пул: workers горутин читают один канал задач, пишут в
// один канал результатов; канал результатов закрывает отдельная горутина
// после wg.Wait. Результат кладётся по индексу задачи — так порядок
// сохраняется, в каком бы порядке ни пришли ответы.
func pool(workers int, jobs []uint64, work func(uint64) uint64) []uint64 {
in := make(chan int)
out := make(chan result)
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range in {
out <- result{i, work(jobs[i])}
}
}()
}
go func() {
for i := range jobs {
in <- i
}
close(in)
}()
go func() {
wg.Wait()
close(out)
}()
res := make([]uint64, len(jobs))
for r := range out {
res[r.idx] = r.val
}
return res
}
// spin — работа только для процессора: xorshift без памяти и без ожиданий.
//
//go:noinline
func spin(seed uint64) uint64 {
x := seed | 1
for i := 0; i < 3_000_000; i++ {
x ^= x << 13
x ^= x >> 7
x ^= x << 17
}
return x
}
func nap(seed uint64) uint64 {
time.Sleep(20 * time.Millisecond)
return seed
}
// cpuTime — процессорное время всего процесса (все потоки, user + sys).
// Отношение его к настенному времени — сколько ядер в среднем работало на
// нас. Если соседние процессы заняли ядро, это видно здесь, а не только в
// миллисекундах.
func cpuTime() time.Duration {
var ru syscall.Rusage
if err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru); err != nil {
panic(err)
}
return time.Duration(ru.Utime.Nano() + ru.Stime.Nano())
}
// bestOf прогоняет каждую конфигурацию rounds раз ВПЕРЕМЕШКУ (круг за
// кругом по всем конфигурациям), а не подряд: если соседний процесс
// нагрузил машину на секунду, он испортит по одному замеру каждой
// конфигурации, а не все замеры одной.
func bestOf(rounds int, configs []int, f func(int)) map[int]time.Duration {
best := map[int]time.Duration{}
for r := 0; r < rounds; r++ {
for _, c := range configs {
start := time.Now()
f(c)
d := time.Since(start)
if old, ok := best[c]; !ok || d < old {
best[c] = d
}
}
}
return best
}
// ---------------------------------------------------------------- конвейер
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func sqForgot(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
// close(out) — забыли
}()
return out
}
// ------------------------------------------------------------------ fan-in
func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range cs {
wg.Add(1)
go func() {
defer wg.Done()
for v := range c {
out <- v
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
// ------------------------------------------------- конвейер без и с context
// countFrom выдаёт 1, 2, 3, … бесконечно — как чтение из сети или из
// очереди, у которого нет «конца».
func countFrom() <-chan int {
out := make(chan int)
go func() {
for n := 1; ; n++ {
out <- n
}
}()
return out
}
func countFromCtx(ctx context.Context) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := 1; ; n++ {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
// sqCtx слушает ctx.Done() только на отправке, а вход читает через range.
// В связке раздела 11 этого хватает: countFromCtx сам закрывает свой выход по
// отмене. Самостоятельной стадии этого мало — на открытом пустом входе range
// отмену не услышит; это и исправленный вариант с двумя select показаны в
// cancel.go. Здесь функция оставлена как есть, чтобы запись прогона
// runs/internals.txt соответствовала коду.
func sqCtx(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
// takeThree берёт первые три значения и уходит — как обработчик запроса,
// которому хватило первой страницы.
func takeThree(ch <-chan int) []int {
var got []int
for v := range ch {
got = append(got, v)
if len(got) == 3 {
break
}
}
return got
}
// states возвращает состояния горутин, в стеке которых есть одна из функций.
func states(funcs ...string) []string {
buf := make([]byte, 1<<20)
buf = buf[:runtime.Stack(buf, true)]
re := regexp.MustCompile(`^goroutine \d+ \[([^\]]+)\]:`)
var out []string
for _, g := range strings.Split(string(buf), "\n\n") {
m := re.FindStringSubmatch(g)
if m == nil {
continue
}
for _, f := range funcs {
if strings.Contains(g, "main."+f+".func") {
out = append(out, f+": ["+m[1]+"]")
}
}
}
sort.Strings(out)
return out
}
// -------------------------------------------------- группа с первой ошибкой
// group — минимальный аналог errgroup.WithContext из golang.org/x/sync,
// написанный на стандартной библиотеке: первая ошибка отменяет общий
// контекст, Wait ждёт всех и возвращает её. Модуль x/sync здесь не
// подключается — сети нет, а смысл паттерна умещается в двадцать строк.
type group struct {
wg sync.WaitGroup
once sync.Once
err error
cancel context.CancelCauseFunc
}
func withContext(parent context.Context) (*group, context.Context) {
ctx, cancel := context.WithCancelCause(parent)
return &group{cancel: cancel}, ctx
}
func (g *group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.once.Do(func() {
g.err = err
g.cancel(err)
})
}
}()
}
func (g *group) Wait() error {
g.wg.Wait()
g.cancel(g.err)
return g.err
}
// ------------------------------------------------------- запуск подопытных
var (
here, cases, work, goBin string
reOffset = regexp.MustCompile(`\+0x[0-9a-f]+`)
reGID = regexp.MustCompile(`goroutine \d+`)
)
func clean(s string) string {
s = strings.ReplaceAll(s, cases, "<cases>")
s = strings.ReplaceAll(s, runtime.GOROOT(), "<GOROOT>")
s = reOffset.ReplaceAllString(s, "")
s = reGID.ReplaceAllString(s, "goroutine <N>")
s = strings.ReplaceAll(s, "\t", " ")
return strings.TrimRight(s, " \t")
}
func command(dir string, name string, args ...string) (string, int) {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS=", "GOTRACEBACK=single")
out, err := cmd.CombinedOutput()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
return string(out), code
}
// buildAndRun собирает cases/<name> и запускает бинарник с аргументами.
// Если сборка не прошла, возвращает её вывод и код.
func buildAndRun(name string, args ...string) (string, int, bool) {
bin := filepath.Join(work, name)
if out, code := command(cases, goBin, "build", "-o", bin, "./"+name); code != 0 {
return out, code, false
}
out, code := command(cases, bin, args...)
return out, code, true
}
// printLines печатает строки, для которых keep вернул true.
func printLines(out string, keep func(string) bool) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if keep(l) {
fmt.Println(" " + clean(l))
}
}
}
// printLinesTo — как printLines, но первую подходящую строку печатает без
// отступа, продолжая уже начатую строку вывода.
func printLinesTo(out string, keep func(string) bool, first *bool) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if !keep(l) {
continue
}
if *first {
fmt.Println(clean(l))
*first = false
} else {
fmt.Println(" " + clean(l))
}
}
}
func main() {
_, file, _, _ := runtime.Caller(0)
here = filepath.Dir(file)
cases = filepath.Join(here, "cases")
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
var err error
work, err = os.MkdirTemp("", "gopattern")
if err != nil {
panic(err)
}
defer os.RemoveAll(work)
fmt.Printf("%s %s/%s | паттерны конкурентности: пул воркеров, конвейер, fan-in\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
fmt.Printf(" GOMAXPROCS=%d, NumCPU=%d\n", runtime.GOMAXPROCS(0), runtime.NumCPU())
// ── 1 ──────────────────────────────────────────────────────────────
show("1) ПУЛ ВОРКЕРОВ: ЗАДАЧА ДЛЯ ПРОЦЕССОРА")
cpuJobs := make([]uint64, 16)
for i := range cpuJobs {
cpuJobs[i] = uint64(i + 1)
}
want := pool(1, cpuJobs, spin)
type cfg struct{ procs, workers int }
type sample struct{ wall, cpu time.Duration }
cfgs := []cfg{{2, 1}, {2, 2}, {2, 4}, {2, 8}, {1, 1}, {1, 8}}
best := map[cfg]sample{}
for round := 0; round < 15; round++ {
for _, c := range cfgs {
runtime.GOMAXPROCS(c.procs)
c0 := cpuTime()
t0 := time.Now()
got := pool(c.workers, cpuJobs, spin)
s := sample{time.Since(t0), cpuTime() - c0}
runtime.GOMAXPROCS(2)
if fmt.Sprint(got) != fmt.Sprint(want) {
panic("пул вернул другие результаты")
}
if old, ok := best[c]; !ok || s.wall < old.wall {
best[c] = s
}
}
}
fmt.Println(" 16 задач, каждая — 3 000 000 шагов xorshift; лучший из 15 кругов вперемешку")
fmt.Println(" GOMAXPROCS воркеров время ускорение к 1 воркеру ядер занято (CPU/время)")
for _, c := range cfgs {
b := best[c]
one := best[cfg{c.procs, 1}]
fmt.Printf(" %6d %5d %-8s %-21.1f %.1f\n", c.procs, c.workers, ms(b.wall),
float64(one.wall)/float64(b.wall), float64(b.cpu)/float64(b.wall))
}
fmt.Println(" Go-код одновременно выполняют не больше GOMAXPROCS потоков; столбец")
fmt.Println(" «ядер занято» — сколько из них в среднем досталось программе на деле.")
// ── 2 ──────────────────────────────────────────────────────────────
show("2) ПУЛ ВОРКЕРОВ: ЗАДАЧА СО СНОМ")
napJobs := make([]uint64, 16)
sl := bestOf(3, []int{1, 2, 4, 8, 16}, func(w int) { pool(w, napJobs, nap) })
fmt.Println(" 16 задач, каждая — time.Sleep(20 мс); лучший из 3 кругов")
fmt.Println(" воркеров время ускорение к 1 воркеру")
for _, w := range []int{1, 2, 4, 8, 16} {
fmt.Printf(" %5d %-8s %.1f\n", w, ms(sl[w]), float64(sl[1])/float64(sl[w]))
}
fmt.Println(" Спящая горутина процессор не занимает: ускорение растёт")
fmt.Println(" с числом воркеров, пока их не станет столько же, сколько задач.")
// ── 3 ──────────────────────────────────────────────────────────────
show("3) ПОРЯДОК РЕЗУЛЬТАТОВ")
names := []string{"a", "b", "c", "d"}
dur := []time.Duration{80, 20, 60, 40}
fmt.Println(" задачи a b c d, работают 80, 20, 60, 40 мс; 4 воркера")
var arrived, byIndex []string
same := 0
for rep := 0; rep < 20; rep++ {
in := make(chan int)
out := make(chan int)
var wg sync.WaitGroup
for w := 0; w < 4; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range in {
time.Sleep(dur[i] * time.Millisecond)
out <- i
}
}()
}
go func() {
for i := range names {
in <- i
}
close(in)
}()
go func() { wg.Wait(); close(out) }()
var order []string
res := make([]string, len(names))
for i := range out {
order = append(order, names[i])
res[i] = strings.ToUpper(names[i])
}
if rep == 0 {
arrived, byIndex = order, res
}
if fmt.Sprint(order) == fmt.Sprint(arrived) {
same++
}
}
fmt.Printf(" в порядке прихода: %v\n", arrived)
fmt.Printf(" res[i] по индексу задачи: %v\n", byIndex)
fmt.Printf(" порядок прихода совпал с первым в %d повторах из 20\n", same)
// ── 4 ──────────────────────────────────────────────────────────────
show("4) КОНВЕЙЕР: GEN → SQ → ПОТРЕБИТЕЛЬ")
before := settle()
var got []int
for v := range sq(gen(1, 2, 3, 4, 5)) {
got = append(got, v)
}
fmt.Printf(" потребитель получил: %v\n", got)
fmt.Println(" range закончился: стадия gen закрыла свой выход, range в sq")
fmt.Println(" вышел, sq закрыла свой — сигнал «больше не будет» прошёл по цепочке")
fmt.Printf(" горутин после конвейера: %+d\n", settle()-before)
// ── 5 ──────────────────────────────────────────────────────────────
show("5) СТАДИЯ ЗАБЫЛА CLOSE: ВСЕ СПЯТ")
out, code, _ := buildAndRun("forgotclose")
printLines(out, func(l string) bool { return strings.TrimSpace(l) != "" })
fmt.Printf(" код выхода: %d\n", code)
// ── 6 ──────────────────────────────────────────────────────────────
show("6) WG.WAIT ДО ЧТЕНИЯ РЕЗУЛЬТАТОВ")
out, code, _ = buildAndRun("waitbeforeread")
printLines(out, func(l string) bool {
return strings.HasPrefix(l, "fatal error") || strings.HasPrefix(l, "goroutine ")
})
fmt.Printf(" код выхода: %d\n", code)
fmt.Println()
fmt.Println(" cases/waitbuffered — та же ошибка, но буфер результатов на 6:")
for _, n := range []string{"6", "7"} {
out, code, _ = buildAndRun("waitbuffered", n)
fmt.Printf(" задач %s: ", n)
first := true
printLinesTo(out, func(l string) bool {
return strings.HasPrefix(l, "задач") || strings.HasPrefix(l, "fatal error")
}, &first)
fmt.Printf(" код выхода: %d\n", code)
}
// ── 7 ──────────────────────────────────────────────────────────────
show("7) СТАДИЯ ЗАБЫЛА CLOSE, НО В ПРОГРАММЕ ЕСТЬ ТАЙМЕР")
before = settle()
done := make(chan []int)
go func() {
var got []int
for v := range sqForgot(gen(1, 2, 3, 4, 5)) {
got = append(got, v)
}
done <- got // сюда не дойдём
}()
select {
case g := <-done:
fmt.Println(" потребитель закончил:", g)
case <-time.After(500 * time.Millisecond):
fmt.Println(" через 500 мс потребитель всё ещё в range; fatal error нет")
}
fmt.Printf(" горутин висит: %+d\n", settle()-before)
// states("main") находит горутины, запущенные из main.main, — здесь это
// потребитель, а не сама главная горутина.
for _, s := range states("main") {
fmt.Println(" потребитель, запущенный из main.main, " + strings.TrimPrefix(s, "main: "))
}
fmt.Println(" Пока есть таймер или работающая горутина, рантайм не считает")
fmt.Println(" это взаимной блокировкой: программа не падает, а висит.")
// ── 8 ──────────────────────────────────────────────────────────────
show("8) FAN-IN: СЛИЯНИЕ КАНАЛОВ")
before = settle()
n, sum := 0, 0
for v := range merge(gen(1, 2, 3, 4), gen(5, 6, 7, 8), gen(9, 10, 11, 12)) {
n++
sum += v
}
fmt.Printf(" три источника по 4 значения: получено %d, сумма %d\n", n, sum)
fmt.Println(" range закончился: close(out) стоит после wg.Wait()")
fmt.Printf(" горутин после слияния: %+d\n", settle()-before)
fmt.Println()
fmt.Println(" cases/earlyclose — close(out) сразу, без wg.Wait():")
out, code, _ = buildAndRun("earlyclose")
printLines(out, func(l string) bool { return strings.TrimSpace(l) != "" })
fmt.Printf(" код выхода: %d\n", code)
// ── 9 ──────────────────────────────────────────────────────────────
show("9) СЕМАФОР НА БУФЕРИЗОВАННОМ КАНАЛЕ")
semRun := func(limit int) (peak, peakG int64, d time.Duration) {
var sem chan struct{}
if limit > 0 {
sem = make(chan struct{}, limit)
}
var inflight, maxIn, maxG atomic.Int64
var wg sync.WaitGroup
base := int64(runtime.NumGoroutine())
start := time.Now()
for i := 0; i < 12; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if sem != nil {
sem <- struct{}{}
defer func() { <-sem }()
}
cur := inflight.Add(1)
for {
m := maxIn.Load()
if cur <= m || maxIn.CompareAndSwap(m, cur) {
break
}
}
if g := int64(runtime.NumGoroutine()) - base; g > maxG.Load() {
maxG.Store(g)
}
time.Sleep(20 * time.Millisecond)
inflight.Add(-1)
}()
}
wg.Wait()
return maxIn.Load(), maxG.Load(), time.Since(start)
}
fmt.Println(" 12 горутин, каждая спит 20 мс; лучший по времени из 3 кругов")
fmt.Println(" семафор одновременно работали горутин было время")
for _, limit := range []int{3, 0} {
var p, g int64
bestD := time.Duration(1 << 62)
for r := 0; r < 3; r++ {
pp, gg, d := semRun(limit)
if pp > p {
p = pp
}
if gg > g {
g = gg
}
if d < bestD {
bestD = d
}
}
label := "make(chan, 3)"
if limit == 0 {
label = "нет"
}
fmt.Printf(" %-16s %-23d %-14d %s\n", label, p, g, ms(bestD))
}
fmt.Println(" Семафор ограничивает работу, а не число горутин: все 12 созданы,")
fmt.Println(" и пока три работают, остальные стоят на sem <- struct{}{}.")
// ── 10 ─────────────────────────────────────────────────────────────
show("10) ПОТРЕБИТЕЛЬ УШЁЛ РАНЬШЕ: БЕЗ CONTEXT")
before = settle()
fmt.Printf(" takeThree(sq(countFrom())) -> %v\n", takeThree(sq(countFrom())))
fmt.Printf(" горутин после: %+d\n", settle()-before)
for _, s := range states("countFrom", "sq") {
fmt.Println(" " + s)
}
// ── 11 ─────────────────────────────────────────────────────────────
show("11) ПОТРЕБИТЕЛЬ УШЁЛ РАНЬШЕ: С CONTEXT")
before = settle()
func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
fmt.Printf(" takeThree(sqCtx(ctx, countFromCtx(ctx))) -> %v\n",
takeThree(sqCtx(ctx, countFromCtx(ctx))))
}()
fmt.Printf(" горутин после defer cancel(): %+d\n", settle()-before)
// ── 12 ─────────────────────────────────────────────────────────────
show("12) ОСТАНОВКА ПО ПЕРВОЙ ОШИБКЕ")
before = settle()
errBad := errors.New("плохая запись")
g, ctx := withContext(context.Background())
parsed := make(chan int)
var seen []int
g.Go(func() error { // источник + разбор: на 4-й записи ошибка
defer close(parsed)
for n := 1; ; n++ {
if n == 4 {
return fmt.Errorf("запись %d: %w", n, errBad)
}
select {
case parsed <- n * n:
case <-ctx.Done():
return nil
}
}
})
for w := 0; w < 3; w++ { // три обработчика, которые ещё и ждут отмены
g.Go(func() error {
for {
select {
case v, ok := <-parsed:
if !ok {
return nil
}
_ = v
case <-ctx.Done():
return nil
}
}
})
}
g.Go(func() error { // долгая независимая работа, которую надо прервать
select {
case <-time.After(10 * time.Second):
seen = append(seen, -1)
return nil
case <-ctx.Done():
return nil
}
})
start := time.Now()
werr := g.Wait()
fmt.Printf(" Wait вернул: %v\n", werr)
fmt.Printf(" errors.Is(err, errBad): %v\n", errors.Is(werr, errBad))
fmt.Printf(" context.Cause(ctx): %v\n", context.Cause(ctx))
fmt.Printf(" долгая работа на 10 с прервана: %v (Wait занял меньше секунды: %v)\n",
len(seen) == 0, time.Since(start) < time.Second)
fmt.Printf(" горутин после: %+d\n", settle()-before)
// ── 13 ─────────────────────────────────────────────────────────────
show("13) SYNC.WAITGROUP.GO")
doc, _ := command(cases, goBin, "doc", "sync.WaitGroup.Go")
first := strings.SplitN(strings.TrimSpace(doc), "\n", 2)[0]
if strings.HasPrefix(first, "package") {
for _, l := range strings.Split(doc, "\n") {
if strings.HasPrefix(l, "func ") {
first = l
break
}
}
}
fmt.Println(" go doc sync.WaitGroup.Go -> " + first)
out, code, ran := buildAndRun("wggo")
if ran {
fmt.Println(" cases/wggo собран и запущен:")
} else {
fmt.Println(" cases/wggo не собирается:")
}
printLines(out, func(l string) bool { return strings.TrimSpace(l) != "" && !strings.HasPrefix(l, "#") })
fmt.Printf(" код выхода: %d\n", code)
vet, _ := command(cases, goBin, "vet", "./wggo")
fmt.Println(" go vet ./wggo (go.mod объявляет go 1.24):")
printLines(vet, func(l string) bool { return strings.TrimSpace(l) != "" && !strings.HasPrefix(l, "#") })
}