Ответ
Планировщик Go реализует модель мультиплексирования M:N, где M горутин выполняются на N потоках операционной системы (ОС). Это позволяет эффективно управлять тысячами горутин, используя небольшое количество системных потоков.
Ключевые компоненты планировщика (G-M-P):
- G (Goroutine): Сама горутина. Это легковесный поток, имеющий свой стек, указатель на текущую инструкцию и другие метаданные.
- M (Machine): Системный поток ОС, управляемый ядром. Это реальный исполнитель кода.
- P (Processor): Контекст для выполнения. P — это ресурс, который необходим потоку M для выполнения горутины G. У каждого P есть своя локальная очередь исполняемых горутин (LRQ - Local Runnable Queue).
Процесс работы:
- Планировщик привязывает контекст P к потоку M.
- Поток M начинает выполнять горутины из локальной очереди своего P.
- Количество P по умолчанию равно количеству ядер CPU и регулируется переменной окружения
GOMAXPROCS
. ИменноGOMAXPROCS
ограничивает, сколько потоков M могут одновременно исполнять Go-код.
// Устанавливает максимальное количество потоков ОС для выполнения Go-кода.
// С версии Go 1.5 это значение по умолчанию равно количеству ядер CPU,
// и менять его вручную требуется крайне редко.
runtime.GOMAXPROCS(runtime.NumCPU())
Work-Stealing (Воровство задач)
Если локальная очередь горутин у одного P опустела, его поток M не простаивает. Он может "украсть" половину горутин из очереди другого P и начать их выполнять. Это обеспечивает равномерную загрузку всех процессорных ядер.
Блокирующие системные вызовы
Когда горутина выполняет блокирующий системный вызов (например, чтение файла с диска), планировщик отсоединяет поток M от его P и может запустить другой поток M для выполнения других горутин из очереди этого P. Это предотвращает блокировку всех горутин из-за одной операции ввода-вывода.