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

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

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

Senior ~24 мин 8 вопросов

Планировщик Go (GMP-модель): вопросы senior-собеседований с ответами

Senior-секция по конкурентности почти всегда доходит до устройства планировщика: GMP-модель, вытеснение, поведение в системных вызовах. Это вопросы не «синтаксиса», а проектирования под нагрузку — от ответов ждут связки «механизм → следствие для продакшена». Конкурентность в практике — продвинутый курс Go и фоновые задачи on asynq; здесь — теория, которую спрашивают.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

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)
}

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: