Deep Engineering

MEASUREMENT

bench/gochan/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/channels
Run on
go1.24.7 linux/amd64, Intel Xeon 2.10GHz, GOMAXPROCS = 2
How to run it
go run bench/gochan/behaviour.go   # работает и из корня
go run bench/gochan/leakdetect.go
cd bench/gochan
go test -run '^$' -bench . -benchmem -count=3 .

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

Замеры для статьи «Канал в Go: прямая передача, честный select и цена буфера»

Файл Что делает
behaviour.go восемь наблюдений без единого замера времени: канал как указатель на структуру с мьютексом, прямая передача мимо буфера, равномерность select и отсутствие приоритета у отмены, закрытый канал, nil-канал, незакрываемая утечка горутин, буфер как отсрочка, порядок значений
cost_test.go цена: что даёт буфер, счётчик через atomic/мьютекс/канал, цена лишней ветки select, канал против мьютекса без парковки, размер элемента
leakdetect.go чем обнаруживают утечку горутин: runtime.NumGoroutine, профиль goroutine из runtime/pprof, проверка «до/после» в тесте; и проверка того, что go test -race утечку не ловит. Ни одного замера времени

Каталог — отдельный модуль Go, поэтому тесты запускаются из него:

go run bench/gochan/behaviour.go   # работает и из корня
go run bench/gochan/leakdetect.go
cd bench/gochan
go test -run '^$' -bench . -benchmem -count=3 .

-count=3 не украшение: все числа ниже — медианы трёх прогонов, и разбросы тоже из них. Один прогон не даёт ни того, ни другого.

behaviour.go помечен //go:build ignore — он package main, а рядом лежит тест пакета gochan, и без метки go test ./... спотыкался бы о два пакета в одном каталоге.

Что здесь важно прочитать правильно

Главное — не наносекунды. Оно в behaviour.go: значение при готовом получателе не проходит через буфер; select перемешивает ветки по-настоящему, и поэтому отменённый контекст выигрывает выбор только в половине случаев; заблокированная навсегда горутина не собирается никогда. Это утверждения о поведении, и проверяются они счётчиками и сравнением, а не секундомером.

По времени сопоставляются только строки внутри одного блока cost_test.go. В каждом блоке все строки дают одинаковый результат, отличается лишь способ. Блоки друг с другом не сопоставляются — и здесь это не формальность, а главная ловушка темы: канал стоит принципиально по-разному в зависимости от того, приходится ли парковать и будить горутину. В блоках 1 и 2 приходится (там работают две горутины), в блоках 3–5 нет (всё происходит в одной). Числа из них несопоставимы, хотя измеряют «канал».

Одна и та же работа нарочно повторена в блоках 3 и 4: BenchmarkSelectNone и BenchmarkNoParkChannel кладут значение в буфер и забирают обратно, отличаясь только размером буфера (1 против 2). Так у каждого блока есть своя точка отсчёта внутри него, и ни одно сравнение не приходится вести через границу блока. Близость их чисел (49,85 против 50,48) — заодно и признак, что замер повторяем.

Что получилось (go1.24.7 linux/amd64, Intel Xeon 2.10GHz, GOMAXPROCS = 2)

Устройство

unsafe.Sizeof(chan int) = 8 байт: в переменной лежит один указатель на hchan в рантайме. Для сравнения: срез — 24 байта, карта — 8, строка — 16.

Внутри hchan — кольцевой буфер, две очереди ожидающих горутин и обычный mutex. Канал не «без блокировок»: каждая отправка и каждый приём берут этот мьютекс.

Прямая передача

Проверяется через len(ch) у канала с буфером на 4:

что сделали len(ch) после отправки
получатель уже ждал 0
получателя не было 1

Если бы значение легло в буфер, длина стала бы 1. Она осталась нулём — значение ушло мимо буфера, прямо в стек получателя.

select

30 000 выборов из трёх готовых каналов:

ветка выбрана доля ожидание
a 10 144 33,8 % 33,3 %
b 9 867 32,9 % 33,3 %
d 9 989 33,3 % 33,3 %

Перемешивание настоящее — в отличие от порядка обхода карты, где «случайность» оказывается сдвигом одной последовательности.

30 000 заходов, где контекст уже отменён, а работа готова:

выбрана раз доля
отмена 14 992 50,0 %
работа 15 008 50,0 %

Отмена не имеет приоритета. Цикл, который «на отмене сразу выходит», при непрерывно готовой работе выходит с вероятностью 1/2 на каждом заходе.

Генератор здесь не засевается, поэтому точные счётчики от запуска к запуску меняются (в контрольном перезапуске вышло 15 011 против 14 989). Повторяется не число, а доля.

Цена

Блок 1 — передать N значений из одной горутины в другую, отличается только размер буфера:

буфер ns/op
0 208,0
1 163,2
8 89,65
64 57,91 лучшая
1024 78,97 хуже, чем при 64

Зависимость не монотонна: из пяти замеренных размеров лучший — не самый большой. Буфер на 1024 значения проигрывает буферу на 64 в 1,36 раза.

Почему после 64 становится хуже, замер НЕ показывает. Правдоподобная догадка — кэш-след кольца (8 КБ против 512 байт), но 8 КБ помещаются и в L1d, так что это гипотеза, а не измерение, и здесь она не проверялась.

Блок 2 — увеличить общий счётчик N раз:

способ ns/op к atomic
atomic.AddInt64 6,553 ×1,00
мьютекс 15,80 ×2,41
канал 67,19 ×10,25

Блок 3 — положить значение в буферизованный канал и забрать обратно; отличается только число веток select, из которых готова ровно одна:

как написан приём ns/op к предыдущей строке
без select 49,85
select из 1 ветки 51,57 +1,7
select из 2 веток 87,17 +35,6
select из 4 веток 158,7 +71,5
select из 8 веток 391,4 +232,7

Одна ветка почти бесплатна — и не потому, что select дёшев, а потому, что его там нет: select с одной веткой и без default компилятор разворачивает в обычную операцию. В cmd/compile/internal/walk/select.go это записано комментарием «optimization: one-case select: single op», и selectgo не вызывается вовсе. Настоящий select начинается со второй ветки, и первое же его появление стоит +35,6 нс — почти три четверти цены самого приёма (49,85). До четырёх веток каждая следующая стоит примерно те же 36 нс (+71,5 на две), а на восьми цена ветки вырастает более чем в полтора раза: +232,7 на четыре, то есть по 58 против 36.

Механизм в runtime/select.go: на каждом входе ветки перемешиваются, затем сортируются по адресу канала — комментарий там гласит: «simple heap sort, to guarantee n log n time» — и блокируются все каналы, а не только тот, что сработает (sellock пропускает повтор, поэтому «восемь захватов» верно для восьми РАЗНЫХ каналов, как здесь). Ничего между заходами не сохраняется.

Блок 4 — то же значение положить и забрать без переключения горутин:

чем защищено ns/op
мьютекс 32,36 ×1,00
канал 50,48 ×1,56
select с default 52,83 ×1,63

Даже когда парковать некого, канал дороже мьютекса в 1,56 раза: он и есть мьютекс плюс очередь и кольцевой буфер. Строку с default надо читать с поправкой: select с одним случаем и default компилятор тоже разворачивает — «optimization: two-case select but one is default: single non-blocking op», — то есть она меряет selectnbrecv, а не selectgo.

Блок 5 — прогнать значение через буфер, отличается тип элемента:

элемент размер ns/op
struct{} 0 б 46,66
int64 8 б 49,79
[128]byte 128 б 59,96
*payload 8 б 49,97

Значение копируется дважды — в буфер и из него. 128 байт обходятся на 20 % дороже восьми; указатель стоит ровно столько же, сколько int64.

От прогона к прогону (три прогона по 1 с): блок 1 — 207,0–214,8 / 158,1–166,5 / 85,63–94,02 / 57,51–59,00 / 77,68–79,84; блок 2 — 6,512–7,052 / 15,79–15,95 / 67,14–67,71; блок 3 — 49,82–50,69 / 51,56–52,15 / 86,68–87,97 / 157,8–159,6 / 390,9–397,5; блок 4 — 32,26–32,95 / 50,42–50,99 / 52,60–53,86; блок 5 — 46,54–46,88 / 49,69–50,84 / 59,87–61,92 / 49,87–50,47. Диапазоны буфера 8 и буфера 64 не пересекаются, буфера 64 и буфера 1024 — тоже. Столбцы B/op и allocs/op везде нулевые.

Наблюдения behaviour.go

  • Канал — указатель на структуру с мьютексом. Восемь байт в переменной.
  • Если получатель уже ждёт, значение минует буфер — видно по len(ch).
  • select перемешивает ветки по-настоящему, и у отмены нет приоритета.
  • Закрытый канал: приём отдаёт остаток, потом нули с ok = false; отправка и повторный close паникуют. Отсюда правило «закрывает отправитель».
  • nil-канал блокирует навсегда, и это рабочий приём: присвоение nil выключает ветку select. close(nil) паникует.
  • Заблокированная горутина не собирается никогда: 100 горутин, ждущих на nil-канале, пережили две сборки мусора.
  • Буфер не делает отправку асинхронной — он отсрочка на фиксированное число значений.
  • Порядок сохраняется у каждого отправителя по отдельности, между отправителями его нет.

Что не подтвердилось

«Канал — это про производительность», «канал вместо мьютекса». Канал — это про владение значением и про то, кого будить; по цене он проигрывает мьютексу всегда, и на переключении горутин (×4,25 в блоке 2), и без него (×1,56 в блоке 4). Выбирают его не за скорость.

«Буфер побольше — надёжнее». Замер показывает обратное: из пяти замеренных размеров лучший — не самый большой, и после 64 значений становится хуже. И behaviour.go показывает, почему в принципе: буфер — не развязка, а отсрочка. Как только он полон, отправитель блокируется ровно так же, как без буфера; большой буфер лишь отодвигает момент и прячет несоответствие скоростей.

«select попробует ветки сверху вниз» и производное от него «case <-ctx.Done() первым — значит, приоритетнее». Замерено: 14 992 против 15 008.

«Канал без буфера — это синхронно, с буфером — асинхронно». Синхронность у канала без буфера и правда есть: отправка не завершится, пока получатель не возьмёт. Асинхронности у буферизованного нет — есть ограниченная отсрочка.

Оговорка к самим числам: две горутины на двух ядрах (GOMAXPROCS = 2). Стоимость передачи между горутинами зависит от того, попали ли они на разные процессоры и приходится ли планировщику будить поток; на машине с другим числом ядер соотношения в блоке 1 будут другими. Блоки 3–5 работают в одной горутине и от этого почти не зависят.

Канал как ребро синхронизации: happens-before (цитаты, без замера)

Запускаемого примера здесь нет и не нужно: это утверждения о том, что ГАРАНТИРОВАНО, а не о том, что происходит на одном прогоне. Прогон не умеет отличить «гарантировано» от «сегодня совпало» — на то и модель памяти.

Первоисточник — The Go Memory Model, раздел Channel communication. Он начинается с рамки:

Channel communication is the main method of synchronization between goroutines. Each send on a particular channel is matched to a corresponding receive from that channel, usually in a different goroutine.

(пер.: «Обмен по каналам — основной способ синхронизации между горутинами. Каждая отправка в конкретный канал сопоставлена соответствующему приёму из этого канала, обычно в другой горутине».)

Дальше три правила. Это и есть то, чем канал отличается от очереди.

Правило 1 — отправка.

A send on a channel is synchronized before the completion of the corresponding receive from that channel.

(пер.: «Отправка в канал синхронизирована раньше завершения соответствующего приёма из этого канала».)

Спецификация тут же разбирает следствие на примере: запись в a идёт до отправки в c, отправка синхронизирована раньше завершения приёма, приём — до print, и поэтому программа гарантированно печатает hello, world. Цепочка строится из двух разных отношений: sequenced before внутри одной горутины и synchronized before через канал.

Правило 2 — закрытие.

The closing of a channel is synchronized before a receive that returns a zero value because the channel is closed.

(пер.: «Закрытие канала синхронизировано раньше приёма, который возвращает нулевое значение из-за того, что канал закрыт».)

Оговорка в конце существенна: ребро даёт не любой приём из закрытого канала, а именно тот, который вернул НУЛЬ ПОТОМУ ЧТО канал закрыт. Приём, забравший реально отправленное значение, работает по правилу 1.

Отсюда же и то, почему close — полноценный способ оповещения: в примере спецификации замена c <- 0 на close(c) даёт программу с той же гарантией.

Правило 3 — приём из небуферизованного канала.

A receive from an unbuffered channel is synchronized before the completion of the corresponding send on that channel.

(пер.: «Приём из небуферизованного канала синхронизирован раньше завершения соответствующей отправки в этот канал».)

Это правило смотрит в ОБРАТНУЮ сторону по сравнению с правилом 1, и оно есть только у небуферизованного канала. Практический смысл: отправитель, вернувшись из ch <- v, знает, что получатель уже забрал значение. Модель памяти подчёркивает границу прямо: если сделать канал буферизованным (c = make(chan int, 1)), та же программа гарантии уже не даёт — «It might print the empty string, crash, or do something else».

Правило 4 — обобщение на буфер.

The kth receive from a channel with capacity C is synchronized before the completion of the k+Cth send on that channel.

(пер.: «k-й приём из канала ёмкостью C синхронизирован раньше завершения k+C-й отправки в этот канал».)

Это правило в статье уже процитировано и разобрано (буфер как разрешённое отставание ровно на C значений).

Чем это отличается от «канала как очереди». Очередь отвечает на вопрос «какие значения и в каком порядке». Все четыре правила выше — не про значения вообще: в них ни слова о том, ЧТО передано. Они про то, какие записи в ПАМЯТИ, сделанные до отправки, обязаны быть видны после приёма. Канал, передавший struct{}{}, не передал ни одного бита полезных данных и при этом сделал всю работу: установил ребро, после которого чужие записи стали видимыми. Именно поэтому канал заменяет мьютекс, а очередь — нет.

Расхождение в тексте, которое стоит знать

Формулировка правила 4 в go.dev/ref/mem и в копии, которая едет вместе с go1.24.7 ($(go env GOROOT)/doc/go_mem.html), РАЗНАЯ.

Онлайн-версия (и то, что процитировано в статье):

The kth receive from a channel with capacity C is synchronized before the completion of the k+Cth send on that channel.

Локальная копия go1.24.7:

The kth receive on a channel with capacity C is synchronized before the completion of the k+Cth send from that channel completes.

Предлоги переставлены (on/from вместо from/on), и в конце лишнее completes, из-за которого предложение перестаёт сходиться грамматически. Вышестоящий текст, судя по всему, поправили позже. Правила 1–3 в обеих копиях совпадают дословно.

Практический вывод: цитату в статье менять не надо. Она приведена со ссылкой на go.dev/ref/mem и этому источнику соответствует точно. Но если кто-то будет сверять с $(go env GOROOT)/doc/go_mem.html, он увидит другой текст, и это не ошибка статьи.

Обнаружение утечек горутин (leakdetect.go)

Утечка в статье уже показана (behaviour.go, наблюдение про nil-канал: 100 горутин пережили две сборки мусора). Здесь другой вопрос — чем её ОБНАРУЖИВАЮТ.

Чего в стандартной поставке нет: готового «детектора утечек горутин», аналогичного -race. Ни флага, ни пакета. Всё, что есть, — счётчик и профиль, то есть наблюдение, а не диагноз.

Что есть:

средство что показывает
runtime.NumGoroutine() сколько горутин всего. Что утечка ЕСТЬ — да; где — нет
профиль goroutine (runtime/pprof) стеки всех горутин, сгруппированные по месту, с числом в каждой группе. Отвечает «сколько И ГДЕ»
net/http/pprof тот же профиль по HTTP в работающей службе, без пересборки
runtime.Stack(buf, true) дамп стеков всех горутин в память программы
GOTRACEBACK=all + SIGQUIT дамп всех горутин при падении

Документация профиля (runtime/pprof, тип Profile) описывает его одной строкой: goroutine - stack traces of all current goroutines (пер.: «goroutine — трассы стека всех текущих горутин»).

Прогон целиком:

go version: go version go1.24.7 linux/amd64

=== 1. runtime.NumGoroutine: утечка ЕСТЬ, но где — не видно ===
  до запуска:  NumGoroutine = 1
  после 100 утечек: NumGoroutine = 101 (прирост 100)
  после двух runtime.GC(): NumGoroutine = 101
  сборщик их не трогает: горутина — не мусор, она живая и просто ждёт.

=== 2. Профиль goroutine: видно ГДЕ ===
  pprof.Lookup("goroutine").Count() = 101

  первые строки дампа (debug=1); адреса меняются от запуска к запуску:
  | goroutine profile: total 101
  | 100 @ 0x46ddae 0x40c5a5 0x40c152 0x4e94b9 0x474c61
  | #	0x4e94b8	main.leak.func1+0x18	/home/claude/deep-engineering-app/bench/gochan/leakdetect.go:67
  | 
  | 1 @ 0x433151 0x46d19d 0x4d5c91 0x4d5ac5 0x4d28eb 0x4e998c 0x43d1ab 0x474c61
  | #	0x4d5c90	runtime/pprof.writeRuntimeProfile+0xb0	/usr/local/go1.24.7/src/runtime/pprof/pprof.go:796
  | #	0x4d5ac4	runtime/pprof.writeGoroutine+0x44	/usr/local/go1.24.7/src/runtime/pprof/pprof.go:755
  | #	0x4d28ea	runtime/pprof.(*Profile).WriteTo+0x14a	/usr/local/go1.24.7/src/runtime/pprof/pprof.go:377
  ...
  строка с местом утечки:
  | 100 @ 0x46ddae 0x40c5a5 0x40c152 0x4e94b9 0x474c61
  | #	0x4e94b8	main.leak.func1+0x18	/home/claude/deep-engineering-app/bench/gochan/leakdetect.go:67

  число 100 в начале группы — это и есть счётчик застрявших
  горутин на ОДНОМ стеке. Именно это отличает профиль от
  NumGoroutine: он отвечает не «сколько всего», а «сколько и где».

=== 3. go test -race утечку НЕ ловит ===
  тест с утечкой под -race: ПРОШЁЛ
  | ok  	leaky	1.011s
  детектор гонок ищет несинхронизированный доступ к памяти.
  Заблокированная навсегда горутина ни к чему не обращается,
  поэтому ловить ему нечего — и это не недоработка, а другая задача.

=== 4. Проверка до/после на одном runtime: ловит и валит тест ===
  тест со сверкой NumGoroutine: УПАЛ
  | --- FAIL: TestLeaks (0.51s)
  |     leak_test.go:27: утекло горутин: было 2, стало 12
  | FAIL
  | FAIL	leaky	0.516s
  | FAIL
  двадцать строк на стандартной библиотеке — и утечка валит сборку.
  Ровно это автоматизируют библиотеки экосистемы.

=== 5. Контроль: та же программа без утечки ===
  тест, где канал закрывают: ПРОШЁЛ
  | ok  	leaky	0.012s

Что показал прогон:

  1. NumGoroutine фиксирует факт. 1 → 101 после ста утечек, и после двух runtime.GC() по-прежнему 101. Сборщик их не трогает: горутина не мусор, она живая и просто ждёт.
  2. Профиль показывает МЕСТО. В дампе debug=1 строка 100 @ 0x46ddae ..., а под ней — main.leak.func1+0x18 .../leakdetect.go:67. Число в начале группы — это счётчик застрявших на ОДНОМ стеке. Именно это отличает профиль от счётчика: он отвечает не «сколько всего», а «сколько и где». Для поиска утечки нужен именно он.
  3. go test -race утечку НЕ ловит. Тест с десятью навсегда заблокированными горутинами проходит: ok leaky 1.011s. И это не недоработка: детектор гонок ищет несинхронизированный доступ к памяти, а заблокированная навсегда горутина ни к чему не обращается. Ловить ему нечего.
  4. Проверка «до/после» на одном runtime работает и валит тест. Двадцать строк без единой зависимости дают --- FAIL: TestLeaks (0.51s) ... утекло горутин: было 2, стало 12. Контрольный прогон с close(ch) проходит за 0,012 с.

Важная деталь проверки №4, без которой она даёт ложные срабатывания: между запуском горутин и сравнением счётчика нужен цикл ожидания. Горутины, которые ЗАВЕРШАЮТСЯ, должны успеть это сделать — планировщик не обязан довести их до конца к моменту сравнения. В leakdetect.go это цикл до 50 попыток по 10 мс.

Экосистема. go.uber.org/goleak — самая известная библиотека для этой задачи; по документации она сверяет набор горутин до и после теста и валит тест на «лишних», то есть автоматизирует ровно проверку №4, добавляя к ней список известных служебных горутин, которые не считаются утечкой. Здесь она не запускалась: в контейнере нет доступа к прокси модулей, поставить и проверить её было не на чем. Всё, что в прогоне выше, — только стандартная поставка.

Источники

  • Спецификация Go, разделы Channel types, Send statements, Receive operator, Select statements, Close — https://go.dev/ref/spec
  • runtime/chan.go — структура hchan, sendDirect, поведение nil-канала и паники closehttps://go.dev/src/runtime/chan.go
  • runtime/select.go — перемешивание pollorder, сортировка lockorder, sellockhttps://go.dev/src/runtime/select.go
  • The Go Memory Model, раздел Channel communication — четыре правила синхронизации: отправка, закрытие, приём из небуферизованного, обобщение на буфер — https://go.dev/ref/mem
  • $(go env GOROOT)/doc/go_mem.html — копия модели памяти, едущая вместе с go1.24.7; формулировка правила про k-й приём в ней отличается от онлайн-версии
  • runtime/pprof, описание профиля goroutinehttps://go.dev/pkg/runtime/pprof/
  • net/http/pprofhttps://go.dev/pkg/net/http/pprof/
  • Data Race Detector — что детектор гонок ищет и чего не ищет — https://go.dev/doc/articles/race_detector
  • Effective Go, раздел Channels — https://go.dev/doc/effective_go
  • Go Concurrency Patterns: Pipelines and cancellation — «Goroutines are not garbage collected; they must exit on their own» — https://go.dev/blog/pipelines

Script

144 lines
//go:build ignore

// Две задачи урока про каналы: обе отвечают прогоном, а не рассуждением.
//
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ ПОД ПРАКТИКУ. Читателю в задаче говорят «вы ответили
// неверно». Если верный ответ назначен редакцией, то ошибается не читатель, а
// материал. Поэтому обе задачи урока печатаются здесь, и в уроке стоит ровно
// то, что напечатала эта программа.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Закрытый канал. Из него можно читать, и чтение
// не блокируется: возвращается нулевое значение и false вторым результатом. А
// вот запись в закрытый канал и повторное закрытие — паника. Плюс то, что
// путают чаще всего: закрытый канал НЕ теряет уже положенные значения, они
// дочитываются, и только потом начинается нулевое значение.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз небуферизованный канал дороже
// буферизованного при передаче между двумя горутинами. Разница здесь не в
// «скорости канала», а в числе ПАРКОВОК: без буфера каждая передача — это
// остановка одной горутины и пробуждение другой через планировщик.
//
// ПРОТОКОЛ ЗАМЕРА. Тот же, что во всех замерах корпуса: варианты меряются не
// подряд, а кругами (в каждом круге оба), и берётся лучший круг для каждого.
//
// ЗАПУСК:
//
//	go run bench/gochan/practice.go
//
// Файл помечен `//go:build ignore`: он package main, а рядом лежит тест пакета
// gochan, и без метки `go test ./...` спотыкался бы о два пакета в одном
// каталоге.
package main

import (
	"fmt"
	"runtime"
	"testing"
)

// ---------------------------------------------------------------- задача 1

// closedChannel печатает то, что читатель должен предсказать.
//
// Код внутри намеренно записан так же, как он показан в уроке: проверка
// scripts/validate-practice.mjs сверяет показанные строки с этим файлом
// построчно, чтобы читатель предсказывал вывод именно этой программы.
func closedChannel() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	a, ok1 := <-ch
	b, ok2 := <-ch
	c, ok3 := <-ch
	fmt.Println(a, ok1)
	fmt.Println(b, ok2)
	fmt.Println(c, ok3)
	fmt.Println(len(ch), cap(ch))
}

// ---------------------------------------------------------------- задача 2

const messages = 1000

// unbuffered — каждая передача паркует отправителя до прихода получателя.
func unbuffered(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ch := make(chan int)
		done := make(chan struct{})
		go func() {
			for range ch {
			}
			close(done)
		}()
		for n := 0; n < messages; n++ {
			ch <- n
		}
		close(ch)
		<-done
	}
}

// buffered — буфер на всю партию: парковок нет вовсе.
func buffered(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ch := make(chan int, messages)
		done := make(chan struct{})
		go func() {
			for range ch {
			}
			close(done)
		}()
		for n := 0; n < messages; n++ {
			ch <- n
		}
		close(ch)
		<-done
	}
}

const rounds = 7

// nsPerOp — время одной операции с долями наносекунды.
//
// Штатный NsPerOp() возвращает целое; здесь операции крупные, но кратность
// считается делением, и лишняя точность в делимом не мешает.
func nsPerOp(r testing.BenchmarkResult) float64 {
	return float64(r.T.Nanoseconds()) / float64(r.N)
}

func main() {
	fmt.Println("ЗАДАЧА 1 — предсказать вывод")
	fmt.Println()
	closedChannel()
	fmt.Println()

	u, f := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		uu := nsPerOp(testing.Benchmark(unbuffered))
		ff := nsPerOp(testing.Benchmark(buffered))
		if u == 0 || uu < u {
			u = uu
		}
		if f == 0 || ff < f {
			f = ff
		}
	}

	fmt.Printf("ЗАДАЧА 2 — во сколько раз дороже канал без буфера (%d значений)\n", messages)
	fmt.Println()
	fmt.Printf("  make(chan int)             %10.0f нс\n", u)
	fmt.Printf("  make(chan int, %d)       %10.0f нс\n", messages, f)
	fmt.Printf("  кратность                  %10.2f\n", u/f)
	fmt.Printf("  она же округлённо          %10.1f\n", u/f)
	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("  на всю партию парковок нет вовсе — остаётся только запись.")
}