Как работает планировщик Go и как горутины распределяются по потокам ОС?

Ответ

Планировщик Go реализует модель мультиплексирования M:N, где M горутин выполняются на N потоках операционной системы (ОС). Это позволяет эффективно управлять тысячами горутин, используя небольшое количество системных потоков.

Ключевые компоненты планировщика (G-M-P):

  • G (Goroutine): Сама горутина. Это легковесный поток, имеющий свой стек, указатель на текущую инструкцию и другие метаданные.
  • M (Machine): Системный поток ОС, управляемый ядром. Это реальный исполнитель кода.
  • P (Processor): Контекст для выполнения. P — это ресурс, который необходим потоку M для выполнения горутины G. У каждого P есть своя локальная очередь исполняемых горутин (LRQ - Local Runnable Queue).

Процесс работы:

  1. Планировщик привязывает контекст P к потоку M.
  2. Поток M начинает выполнять горутины из локальной очереди своего P.
  3. Количество P по умолчанию равно количеству ядер CPU и регулируется переменной окружения GOMAXPROCS. Именно GOMAXPROCS ограничивает, сколько потоков M могут одновременно исполнять Go-код.
// Устанавливает максимальное количество потоков ОС для выполнения Go-кода.
// С версии Go 1.5 это значение по умолчанию равно количеству ядер CPU,
// и менять его вручную требуется крайне редко.
runtime.GOMAXPROCS(runtime.NumCPU())

Work-Stealing (Воровство задач)

Если локальная очередь горутин у одного P опустела, его поток M не простаивает. Он может "украсть" половину горутин из очереди другого P и начать их выполнять. Это обеспечивает равномерную загрузку всех процессорных ядер.

Блокирующие системные вызовы

Когда горутина выполняет блокирующий системный вызов (например, чтение файла с диска), планировщик отсоединяет поток M от его P и может запустить другой поток M для выполнения других горутин из очереди этого P. Это предотвращает блокировку всех горутин из-за одной операции ввода-вывода.