Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Что такое G, M и P в GMP-модели планировщика?
G — горутина (структура со стеком, состоянием и функцией), M — поток ОС (machine), P — логический процессор: слот исполнения с локальной очередью горутин; их число равно GOMAXPROCS. Правило исполнения: горутина G выполняется потоком M только когда M «держит» P. P нужен, чтобы планирование было локальным: каждый P имеет свою очередь горутин и кэш, и в штатной работе горутины не дерутся за один глобальный список. Существует и глобальная очередь — туда попадают, например, горутины после net-операций и при переполнении локальной очереди (256).
runtime.GOMAXPROCS(runtime.NumCPU()) // обычно настройка не нужна: // в контейнерах Go 1.25+ учитывает лимит CPU автоматически
2. Как работает work stealing?
Когда локальная очередь P пуста, поток ищет работу по порядку: глобальная очередь → сетевой poller (готовые net-горутины) → кража половины очереди у другого случайного P. Кража половины, а не одного элемента — чтобы жертва не обеднела мгновенно и пересылки были редкими. Следствие для практики: перекос «один P забит, остальные пустые» саморазрешается, но с задержкой и ценой миграций горутин между потоками (стек остаётся на месте, но кэши процессора холодные). На senior-ответ хорошо ложится формулировка: «балансировка ленивая и локальная, потому и дешёвая».
3. Горутина делает блокирующий системный вызов (файл, сеть без poller). Что происходит с M и P?
Системный вызов, который может заблокировать поток, обслуживается обёрткой рантайма: если вызов не вернулся быстро, рантайм отцепляет P от этого M и отдаёт P другому потоку — работа программы не останавливается из-за одного застрявшего syscall. Когда вызов завершается, M пытается вернуть себе P; если свободного нет — горутина встаёт в глобальную очередь, а M засыпает. Следствие: программы с большим числом блокирующих файловых операций могут породить больше потоков, чем GOMAXPROCS — это нормально, но стоит мониторить метрику потоков (M) в pprof/goroutine-профилях.
4. А сетевые операции? Почему net.Conn не блокирует поток?
Сетевой I/O в Go построен на неблокирующих сокетах и сетевом поллере (epoll в Linux, kqueue в BSD/macOS, IOCP в Windows). Горутина, вызвавшая Read на неготовом сокете, паркуется: её G регистрируется в поллере, а M освобождается для других горутин. Когда ОС сообщает о готовности, поллер делает горутину runnable — она попадает в очередь P. Именно поэтому одна программа обслуживает десятки тысяч соединений без десятков тысяч потоков: M нужны только для вычислений, ожидание сети живёт в poller-е. Файловый I/O на диске так не работает — там поток реально блокируется.
5. Как происходит вытеснение горутины? Зачем был нужен async preemption?
До Go 1.14 горутина могла «зажать» поток долгим циклом без вызовов функций: вытеснение было кооперативным — по точкам безопасного останова (вызовы функций, аллокации, операции с каналами). С Go 1.14 вытеснение асинхронное: sysmon — фоновый монитор рантайма — замечает горутину, работающую дольше ~10 мс, и шлёт ей сигнал (SIGURG), по которому она останавливается в безопасной точке, и планировщик забирает P. Следствие: плотный вычислительный цикл больше не замораживает GC и другие горутины; но кооперативные точки всё равно важны в горячем пути (сигнал и его обработка стоят времени).
6. Что делает sysmon, кроме вытеснения?
Sysmon — поток-монитор без P, просыпается каждые 10-20 мс и делает работу, которую нельзя доверить спящей программе: отбирает P у потоков, застрявших в длинных syscall; запускает форс-сборку GC, если программа долго не даёт безопасной точки; возвращает память ОС (scavenging); засекает net-pollertimeout. На собеседовании sysmon — хороший маркер глубины: кандидаты, которые о нём помнят, обычно понимали реальные продакшен-проблемы (например, «программа почти не аллоцирует — почему GC не запускался и память росла» — часть ответа именно в sysmon и GOMEMLIMIT).
7. GOMAXPROCS=1 — что изменится? Зачем его вообще трогают в контейнерах?
GOMAXPROCS ограничивает число P — горутины всё равно можно запустить тысячи, но вычисляться одновременно будут не больше GOMAXPROCS штук. В контейнере с лимитом CPU рантайм до Go 1.25 видел все ядра хоста: 32-ядерная машина с лимитом в 1 ядро планировала на 32 P, получая троттлинг от cgroups — латентность скала рваными волнами. Лечили руками: automaxprocs или явный GOMAXPROCS по квоте. С Go 1.25 рантайм учитывает cgroup-лимиты сам. Итоговый ответ: трогать точечно — для CPU-bound батчей на выделенных машинах; в контейнерах — доверять новому рантайму, но знать, откуда проблема.
8. Горутина A крутится в вычислительном цикле, горутина B должна отреагировать на отмену через ctx. Что senior-кандидат обязан сказать?
Три уровня ответа. (1) Отмена кооперативна: контекст не остановит цикл сам — горутина B обязана в цикле проверять ctx.Done() в select; асинхронное вытеснение тут не помощник — оно лишь не даст циклу заморозить остальных. (2) Если B ждёт канала, а продюсер не отменяется — классическая утечка; правильная связка: select по данным и ctx.Done() (как в теме про горутины). (3) Для вычислительных задач на отмену — не «проверка флага раз в итерацию», а явные точки выхода с ctx.Err() и договорённость о частоте проверок (каждые N итераций) в CPU-bound коде. Ответ уровня senior связывает кооперативность отмены, вытеснение и политику ретраев.
for i := 0; i < bigN; i++ {
if i%1024 == 0 {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
compute(i)
}Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Asynq: полный курс по фоновым задачам в Go — Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.