Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Планировщик горутин Go: кто блокируется, кто просыпается

Конкурентность в Go объясняют обычно словами: «горутины легковесные», «каналы синхронизируют». Но на собеседовании спрашивают про другое: что происходит, когда буфер канала заполнен, почему select с несколькими готовыми ветками даёт случайный результат и как выглядит дедлок. Эта симуляция отвечает на такие вопросы пошагово — с журналом событий и состоянием каждой горутины.

3 сценария, буфер 0–3шаг за шагом, без анимации

Планировщик без магии

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

Самое интересное происходит в момент блокировки. Небуферизованный канал — это рукопожатие: отправитель блокируется до тех пор, пока получатель не придёт за значением, и наоборот. Буферизованный канал разрывает эту связь: отправитель оставляет значение в очереди и уходит дальше, пока очередь не переполнится. Разница видна не в теории, а в скорости: с буфером производитель не ждёт потребителя на каждом шаге.

Ниже — симулятор. Переключайте сценарий, число горутин и ёмкость буфера, затем нажимайте «Шаг вперёд» и следите за журналом: каждая строка объясняет, что сделала горутина и почему заблокировалась. Обратите внимание на сценарий с ёмкостью 0 — в нём отправитель и получатель встречаются в одной точке, и порядок шагов меняет всю картину.

Настройки сценария

Меняйте сценарий, число горутин и ёмкость буфера — симуляция начнётся заново.

Сценарий

Первая горутина шлёт значения в канал, остальные забирают их. С ёмкостью 0 вы увидите классическое рукопожатие, с ёмкостью 2 — как данные копятся в буфере.

Сделано шагов: 0

Каналы

chнебуферизованныйв очереди: 0
буфера нет: отправка и приём должны встретиться

Горутины

Состояние горутин: программа, текущая операция и статус блокировки
ГорутинаПрограммаСостояниеЖдёт
G1
  1. работа
  2. ch <- 1
  3. ch <- 2
готова к запуску—
G2
  1. v := <-ch
  2. работа
готова к запуску—
G3
  1. v := <-ch
  2. работа
готова к запуску—

Журнал событий

Нажмите «Шаг вперёд», чтобы увидеть, что происходит.

Почему порядок детерминированный: в реальном Go планировщик мультиплексирует горутины на потоки ОС и порядок запуска не гарантирован, а в select при нескольких готовых ветках рантайм выбирает случайную. Здесь очередь и выбор ветки детерминированы — иначе шаг за шагом невозможно объяснить. Правила блокировки и рукопожатия при этом повторяют настоящие.

Частые вопросы о горутинах и каналах

Чем буферизованный канал отличается от небуферизованного?

Небуферизованный канал — это синхронизация рукопожатием: отправка блокируется, пока значение не примут, приём блокируется, пока не отправят. Буферизованный канал — очередь фиксированной ёмкости: отправка проходит сразу, если есть место, и блокируется только при полном буфере. Буфер не «ускоряет» канал сам по себе — он лишь развязывает отправителя и получателя по времени, а ценой становится лишняя память и более сложный разбор утечек.

Что именно происходит, когда горутина блокируется?

Горутина снимается с очереди готовых, её состояние сохраняется, и она встаёт в очередь ожидания на конкретный канал (отправителей и получателей рантайм держит отдельно). Когда условие наступает — в буфере появилось место или кто-то прислал значение — рантайм переводит горутину в состояние «готова» и ставит в конец очереди готовых. Именно поэтому порядок пробуждения в Go гарантирован только для каналов (FIFO), а для select при нескольких готовых ветках выбор случайный.

Как понять, что программа попала в дедлок?

Если все горутины заблокированы и ни одна не может проснуться, рантайм это замечает и печатает fatal error: all goroutines are asleep - deadlock!, после чего программа падает. В продакшене дедлок чаще выглядит иначе: горутина ждёт значения, которого никто не пришлёт, но при этом есть другие живые горутины — тогда рантайм не считает это дедлоком, и утечка обнаруживается только по графику горутин. Лечится context с таймаутом и веткой default в select.

Что делает select, если готовы несколько веток?

Рантайм выбирает одну из готовых веток случайно и равновероятно — это защита от голодания: если бы выбор был предсказуемым, одна ветка могла бы «задушить» другую. Поэтому код вида select с несколькими готовыми каналами нужно писать так, чтобы порядок веток не имел значения, а зависеть от выбора нельзя. Если ни одна ветка не готова, select блокируется до пробуждения; пустой select{} блокирует горутину навсегда.

Что изучить дальше

Что внутри

Три состояния горутины

Горутина либо выполняется на логическом процессоре, либо ждёт в очереди, либо заблокирована на канале — и это разные состояния с разными последствиями. Заблокированная горутина не занимает ядро, но продолжает занимать память, и до тех пор не освободит её, пока канал, на котором она стоит, не закроют или не отправят значение.

Симулятор показывает эти состояния по шагам: видно, кто в очереди на исполнение, кто висит на отправке, кто на приёме, и в какой момент буфер канала стал местом, где данные осели.

Буфер меняет всё

Небуферизованный канал — это рукопожатие: отправитель ждёт получателя, и наоборот. Буферизованный канал разрывает связь во времени: отправка проходит, пока есть место, а получатель забирает позже.

Разница видна не в теории, а в конкретных сценариях: с буфером размера 1 десять воркеров не встанут в очередь, с буфером 0 — встанут. Сценарии симулятора специально построены на этом различии, потому что именно оно чаще всего объясняет, почему сервис «встал» под нагрузкой.

Дедлок — это не зависание

Классическая пара: две горутины держат по одному ресурсу и ждут второй. Ни одна не выполняется, но обе живы: процесс не падает, а просто перестаёт отвечать. В симуляторе можно довести сценарий до дедлока и увидеть, на каком шаге два ожидания встретились.

Как пользоваться

  1. Возьмите свой канал и свой сценарий. Симулятор начинается с чистого листа: добавьте горутины, выберите буфер и запустите — увидите реальный порядок событий, а не идеальную картинку из учебника.
  2. Меняйте один параметр за раз. Сначала запустите с буфером 0, потом с буфером 1. Разница в порядке блокировок обычно и есть ответ на вопрос «почему у меня встало».
  3. Сверьте с кодом, который пишете. Если в коде есть два канала и два воркера — пройдите этот сценарий в симуляторе, прежде чем деплоить.

Когда это пригождается

Сервис перестал отвечать под нагрузкой

Скорее всего, воркеры блокируются на отправке в канал, который никто не читает, или читатель ждёт данные, которые никто не отправляет. Симулятор показывает, где именно возникла блокировка и почему.

Горутин больше, чем ожидалось

Каждая заблокированная горутина — это стек и служебные структуры, которые никогда не освободятся без закрытия канала. Симулятор показывает, сколько горутин осталось в состоянии ожидания при вашей схеме.

select ведёт себя не так, как ожидалось

При нескольких готовых ветвях select выбирает одну случайно. Если ветвей две и обе готовы постоянно, порядок обработки будет скакать — это объясняет плавающие задержки в метриках.

Симулятор объясняет порядок блокировок и ожиданий, но не моделирует планировщик целиком: реальный порядок выполнения горутин на процессоре определяется версией рантайма, нагрузкой и тем, как код скомпилирован.