За счет чего достигается параллелизм и кооперативность в Go
Что проверяют
Знаете ли вы, как устроен планировщик рантайма, а не только слово «горутина», и понимаете ли, что он давно не чисто кооперативный. Следом спрашивают, что будет с горутиной в цикле без вызовов функций и что происходит при блокирующем системном вызове.
Как отвечать
Стройте ответ вокруг модели GMP:
- G: горутина. Стек стартует с 2 КБ и растёт копированием, поэтому их можно держать сотни тысяч;
- M: поток ОС, который исполняет код;
- P: логический процессор с локальной очередью горутин. Их число задаёт
GOMAXPROCS, по умолчанию это количество доступных ядер, а с Go 1.25 на Linux учитывается и лимит CPU из cgroup контейнера.
Параллелизм возникает за счёт того, что Go-код одновременно исполняют до GOMAXPROCS потоков, каждый со своим P. Опустевший P забирает половину очереди у соседа (work stealing), так нагрузка выравнивается.
Кооперативность это точки, где горутина сама отдаёт управление: операции с каналами, ожидание мьютекса, runtime.Gosched(), проверка в прологе функции при вызове. До Go 1.14 этим всё и ограничивалось, и цикл без вызовов мог надолго занять P и задержать сборку мусора. С Go 1.14 есть асинхронное вытеснение: фоновый sysmon замечает горутину, которая работает дольше примерно 10 мс, и прерывает её сигналом. Точнее называть планировщик гибридным.
Без двух механизмов ответ неполный. При блокирующем системном вызове M отпускает свой P, и тот уходит другому потоку, чтобы остальные горутины не стояли. Сетевой ввод-вывод идёт через netpoller на epoll или kqueue: горутина паркуется, поток не блокируется.
Где спотыкаются
Путают конкурентность и параллелизм. Каналы и select это средства синхронизации и композиции, параллельности они сами не дают, её обеспечивают P и потоки.
Говорят, что планировщик полностью кооперативный и зависнуть можно на for {}. Это было верно до версии 1.14, сейчас такой цикл будет вытеснен.
Считают, что горутина равна потоку или что GOMAXPROCS ограничивает число горутин. Он ограничивает число потоков, одновременно исполняющих Go-код, а потоков в блокирующих вызовах может быть больше.