Конкурентность в Go — горутины дешёвы, но не бесплатны
Go построен вокруг одного обещания — конкурентность должна быть настолько дешёвой, чтобы вы перестали её экономить. Запустить горутину стоит дешевле, чем выделить поток ОС: крошечный стартовый стек, переключение в пользовательском пространстве, никакого обращения к ядру. Поэтому идиоматичный Go-код спокойно держит десятки тысяч горутин там, где на потоках ОС это было бы немыслимо.
Но «дёшево» — не «бесплатно» и не «магия». За оператором go стоит планировщик GMP: горутины (G) выполняются на потоках ОС (M) через логические процессоры (P), и понимание этих трёх букв отделяет кандидата, который умеет рассуждать о производительности, от того, кто просто заучил синтаксис. Ловушки начинаются именно там, где интуиция из языков с потоками 1:1 даёт сбой: горутина не привязана к ядру, число P фиксировано через GOMAXPROCS, а число потоков ОС — нет; блокирующий syscall уносит поток в ядро, но не замораживает его очередь; плотный цикл без вызовов функций до Go 1.14 вообще нельзя было вытеснить. Эта тема разбирает рантайм по слоям — от устройства одной горутины до асинхронного вытеснения.
Карта темы
- Устройство горутины — что такое горутина, почему она легче потока ОС и как
runtimeмультиплексирует множество горутин на несколько потоков. - Стек горутины — стартовый размер около 8 КБ и рост копированием в новый непрерывный блок, а не сцеплением сегментов.
- GOMAXPROCS — что именно задаёт эта настройка, почему она ограничивает число P, а не число горутин или потоков ОС.
- Планировщик GMP — три абстракции G, M и P, и правило, по которому M обязан удерживать P, чтобы исполнять код Go.
- Очереди выполнения — зачем у каждого P своя безблокировочная локальная очередь рядом с общей глобальной.
- Обработка syscall — как
runtimeотсоединяет P от потока, ушедшего в блокирующий syscall, чтобы очередь продолжала исполняться. - Work-stealing — как простаивающий P крадёт половину очереди у случайной жертвы и балансирует нагрузку без центрального диспетчера.
- Вытеснение горутин — кооперативное вытеснение на безопасных точках и асинхронное вытеснение через сигнал
SIGURGс Go 1.14.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что каждая горутина — это поток ОС 1:1 | Неверная модель стоимости; не объяснить, почему горутин могут быть сотни тысяч |
| Называть стартовым стеком горутины 1 МБ потока ОС | Завышение в ~128 раз; не понять, почему горутины дёшевы по памяти |
| Думать, что стек растёт сцеплением сегментов | Упустить копирование кадров и переписывание указателей при росте |
Путать GOMAXPROCS с пределом числа горутин | Ложное ожидание, что go заблокируется по достижении лимита |
Считать, что GOMAXPROCS ограничивает число потоков ОС | Не объяснить всплеск потоков при множестве блокирующих syscall |
| Путать, какая буква — поток, а какая — горутина | M это поток ОС, G это горутина; перепутанная модель планировщика |
| Думать, что M исполняет код Go без захвата P | Не понять, почему параллелизм ограничен числом P |
| Считать, что локальная очередь хранит заблокированные горутины | Она хранит готовые к запуску, как и глобальная |
| Полагать, что P остаётся закреплён за потоком в блокирующем syscall | Ложный вывод, что один syscall замораживает всю очередь P |
| Утверждать, что плотный цикл без вызовов невытесняем | Верно лишь до Go 1.14; с тех пор работает асинхронное вытеснение |
| Воображать центральный поток-балансировщик для work-stealing | Не понять, что крадёт сам простаивающий P, и крадёт у случайной жертвы |
Значение для собеседований
Конкурентность — обязательная тема на любом серьёзном Go-интервью, и спрашивают не синтаксис go, а модель исполнения под ним. Интервьюер проверяет, есть ли у вас рабочая ментальная модель планировщика.
Что обычно проверяют:
- Чем горутина отличается от потока ОС — кто ею управляет, где происходит переключение, какой стартовый стек.
- Три абстракции G, M, P и правило «M обязан удерживать P, чтобы исполнять код Go».
- Чем управляет
GOMAXPROCS— числом P, а не горутин и не потоков ОС. - Зачем у каждого P локальная очередь рядом с глобальной — безблокировочный доступ и кэш-локальность.
- Что происходит с P, когда горутина уходит в блокирующий syscall — отсоединение P и передача другому M.
- Как работает work-stealing — простаивающий P сам крадёт половину очереди у случайного P.
- Как
runtimeвытесняет горутину в плотном цикле без вызовов функций — асинхронное вытеснение сигналомSIGURG.
Типичный неверный ответ: «горутина — это просто лёгкий поток, ядро само нарежет им время». Это запускает разбор того, что горутины мультиплексируются на пул потоков самим runtime, а не ядром, и что вытеснение в Go — забота планировщика рантайма, а не планировщика ОС.