Планировщик горутин Go: кто блокируется, кто просыпается
Конкурентность в Go объясняют обычно словами: «горутины легковесные», «каналы синхронизируют». Но на собеседовании спрашивают про другое: что происходит, когда буфер канала заполнен, почему select с несколькими готовыми ветками даёт случайный результат и как выглядит дедлок. Эта симуляция отвечает на такие вопросы пошагово — с журналом событий и состоянием каждой горутины.
Планировщик без магии
Планировщик Go держит две очереди: готовые к запуску горутины и заблокированные. Горутина стартует, выполняется, а затем либо остаётся готовой (переключилась на другую горутину), либо блокируется — и тогда встаёт в очередь ожидания на канале. Количество потоков ОС, на которых крутятся горутины, задаётся GOMAXPROCS: это не число горутин, а число параллельных вычислительных потоков.
Самое интересное происходит в момент блокировки. Небуферизованный канал — это рукопожатие: отправитель блокируется до тех пор, пока получатель не придёт за значением, и наоборот. Буферизованный канал разрывает эту связь: отправитель оставляет значение в очереди и уходит дальше, пока очередь не переполнится. Разница видна не в теории, а в скорости: с буфером производитель не ждёт потребителя на каждом шаге.
Ниже — симулятор. Переключайте сценарий, число горутин и ёмкость буфера, затем нажимайте «Шаг вперёд» и следите за журналом: каждая строка объясняет, что сделала горутина и почему заблокировалась. Обратите внимание на сценарий с ёмкостью 0 — в нём отправитель и получатель встречаются в одной точке, и порядок шагов меняет всю картину.
Настройки сценария
Меняйте сценарий, число горутин и ёмкость буфера — симуляция начнётся заново.
Первая горутина шлёт значения в канал, остальные забирают их. С ёмкостью 0 вы увидите классическое рукопожатие, с ёмкостью 2 — как данные копятся в буфере.
Каналы
Горутины
| Горутина | Программа | Состояние | Ждёт |
|---|---|---|---|
| G1 |
| готова к запуску | — |
| G2 |
| готова к запуску | — |
| G3 |
| готова к запуску | — |
Журнал событий
Нажмите «Шаг вперёд», чтобы увидеть, что происходит.
select при нескольких готовых ветках рантайм выбирает случайную. Здесь очередь и выбор ветки детерминированы — иначе шаг за шагом невозможно объяснить. Правила блокировки и рукопожатия при этом повторяют настоящие.Частые вопросы о горутинах и каналах
Чем буферизованный канал отличается от небуферизованного?
Небуферизованный канал — это синхронизация рукопожатием: отправка блокируется, пока значение не примут, приём блокируется, пока не отправят. Буферизованный канал — очередь фиксированной ёмкости: отправка проходит сразу, если есть место, и блокируется только при полном буфере. Буфер не «ускоряет» канал сам по себе — он лишь развязывает отправителя и получателя по времени, а ценой становится лишняя память и более сложный разбор утечек.
Что именно происходит, когда горутина блокируется?
Горутина снимается с очереди готовых, её состояние сохраняется, и она встаёт в очередь ожидания на конкретный канал (отправителей и получателей рантайм держит отдельно). Когда условие наступает — в буфере появилось место или кто-то прислал значение — рантайм переводит горутину в состояние «готова» и ставит в конец очереди готовых. Именно поэтому порядок пробуждения в Go гарантирован только для каналов (FIFO), а для select при нескольких готовых ветках выбор случайный.
Как понять, что программа попала в дедлок?
Если все горутины заблокированы и ни одна не может проснуться, рантайм это замечает и печатает fatal error: all goroutines are asleep - deadlock!, после чего программа падает. В продакшене дедлок чаще выглядит иначе: горутина ждёт значения, которого никто не пришлёт, но при этом есть другие живые горутины — тогда рантайм не считает это дедлоком, и утечка обнаруживается только по графику горутин. Лечится context с таймаутом и веткой default в select.
Что делает select, если готовы несколько веток?
Рантайм выбирает одну из готовых веток случайно и равновероятно — это защита от голодания: если бы выбор был предсказуемым, одна ветка могла бы «задушить» другую. Поэтому код вида select с несколькими готовыми каналами нужно писать так, чтобы порядок веток не имел значения, а зависеть от выбора нельзя. Если ни одна ветка не готова, select блокируется до пробуждения; пустой select{} блокирует горутину навсегда.
Что изучить дальше
- Урок про горутиныКак запускаются горутины, почему main не ждёт их и что такое GOMAXPROCS.
- Урок про каналыmake(chan T), отправка и приём, буферизованные каналы и закрытие.
- Урок про selectМультиплексирование каналов, default, таймауты через time.After.
- Собеседование: горутины и каналыРазбор вопросов интервьюеров с кодами и объяснениями.
Что внутри
Три состояния горутины
Горутина либо выполняется на логическом процессоре, либо ждёт в очереди, либо заблокирована на канале — и это разные состояния с разными последствиями. Заблокированная горутина не занимает ядро, но продолжает занимать память, и до тех пор не освободит её, пока канал, на котором она стоит, не закроют или не отправят значение.
Симулятор показывает эти состояния по шагам: видно, кто в очереди на исполнение, кто висит на отправке, кто на приёме, и в какой момент буфер канала стал местом, где данные осели.
Буфер меняет всё
Небуферизованный канал — это рукопожатие: отправитель ждёт получателя, и наоборот. Буферизованный канал разрывает связь во времени: отправка проходит, пока есть место, а получатель забирает позже.
Разница видна не в теории, а в конкретных сценариях: с буфером размера 1 десять воркеров не встанут в очередь, с буфером 0 — встанут. Сценарии симулятора специально построены на этом различии, потому что именно оно чаще всего объясняет, почему сервис «встал» под нагрузкой.
Дедлок — это не зависание
Классическая пара: две горутины держат по одному ресурсу и ждут второй. Ни одна не выполняется, но обе живы: процесс не падает, а просто перестаёт отвечать. В симуляторе можно довести сценарий до дедлока и увидеть, на каком шаге два ожидания встретились.
Как пользоваться
- Возьмите свой канал и свой сценарий. Симулятор начинается с чистого листа: добавьте горутины, выберите буфер и запустите — увидите реальный порядок событий, а не идеальную картинку из учебника.
- Меняйте один параметр за раз. Сначала запустите с буфером 0, потом с буфером 1. Разница в порядке блокировок обычно и есть ответ на вопрос «почему у меня встало».
- Сверьте с кодом, который пишете. Если в коде есть два канала и два воркера — пройдите этот сценарий в симуляторе, прежде чем деплоить.
Когда это пригождается
Сервис перестал отвечать под нагрузкой
Скорее всего, воркеры блокируются на отправке в канал, который никто не читает, или читатель ждёт данные, которые никто не отправляет. Симулятор показывает, где именно возникла блокировка и почему.
Горутин больше, чем ожидалось
Каждая заблокированная горутина — это стек и служебные структуры, которые никогда не освободятся без закрытия канала. Симулятор показывает, сколько горутин осталось в состоянии ожидания при вашей схеме.
select ведёт себя не так, как ожидалось
При нескольких готовых ветвях select выбирает одну случайно. Если ветвей две и обе готовы постоянно, порядок обработки будет скакать — это объясняет плавающие задержки в метриках.
Симулятор объясняет порядок блокировок и ожиданий, но не моделирует планировщик целиком: реальный порядок выполнения горутин на процессоре определяется версией рантайма, нагрузкой и тем, как код скомпилирован.