Как планировщик Go распределяет goroutine между потоками
Что проверяют
Знаете ли вы модель из трёх сущностей и понимаете ли, что происходит при блокирующем вызове.
Как отвечать
Планировщик работает по схеме многие ко многим: множество горутин выполняется на меньшем числе потоков операционной системы. Участвуют три сущности:
- G это горутина, то есть задача;
- M это поток операционной системы, исполнитель;
- P это логический процессор, контекст выполнения. Число P задаётся
GOMAXPROCSи по умолчанию равно числу ядер.
Правило связывания: чтобы выполнять горутины, поток должен владеть логическим процессором. Именно P ограничивает параллелизм, а не число потоков. У каждого P есть локальная очередь готовых горутин, плюс существует общая глобальная очередь.
Самое интересное происходит при блокировке:
- блокирующий системный вызов (чтение файла, обращение через C): рантайм отцепляет P от занятого потока и передаёт другому потоку, который продолжает выполнять остальные горутины. Заблокированной остаётся только эта пара из горутины и потока;
- сетевые операции вообще не блокируют поток: они уходят через опрашиватель сети, построенный на механизмах ядра вроде epoll. Горутина снимается с выполнения, а поток берёт следующую. Именно поэтому на Go естественно писать сетевые сервисы в блокирующем стиле, получая при этом эффективность асинхронного;
- блокировка на канале или мьютексе переводит горутину в ожидание и не занимает поток вовсе.
Про вытеснение: раньше планировщик был кооперативным и переключался только в точках вызова функций, поэтому счётный цикл без вызовов мог занять процессор надолго. С версии 1.14 добавлено асинхронное вытеснение через сигнал, и такая ситуация больше не подвешивает планировщик.
Практический вывод: горутины дёшевы, блокирующие вызовы не страшны, а GOMAXPROCS стоит понимать как ограничение параллелизма, особенно в контейнере, где лимит процессора не совпадает с числом ядер узла.
Где спотыкаются
Считают, что горутина это обёртка над потоком.
Думают, что GOMAXPROCS ограничивает число горутин.
Полагают, что блокирующее чтение файла останавливает всю программу.
Не учитывают, что в контейнере число ядер видно хостовое, а лимит другой.