Как планировщик Go распределяет работу между горутинами? Используется ли механизм «work-stealing»?

Ответ

Да, планировщик Go использует эффективную модель M:P:G и механизм work-stealing (воровство работы) для распределения задач.

Модель M:P:G

  • M (Machine): Поток операционной системы.
  • P (Processor): Контекст для выполнения Go-кода. У каждого P есть своя локальная очередь готовых к выполнению горутин (Local Queue).
  • G (Goroutine): Сама горутина.

Планировщик стремится к тому, чтобы на каждом ядре процессора работал один поток M, связанный с одним контекстом P, который выполняет горутины G из своей локальной очереди.

Что происходит при простое и как работает Work-Stealing?

  1. Блокировка горутины: Если горутина G блокируется (например, на системном вызове, I/O или ожидании канала), поток M отсоединяется от своего P и уходит в ожидание вместе с G. Планировщик немедленно берет другой (или создает новый) поток M и привязывает его к P, чтобы тот продолжил выполнять другие горутины из своей очереди. Это предотвращает простой процессорного ядра.

  2. Work-Stealing: Если у контекста P заканчиваются горутины в его локальной очереди, он не простаивает. Вместо этого он становится "вором" и пытается украсть половину горутин из очереди другого, более загруженного P.

Этот механизм обеспечивает:

  • Эффективную утилизацию CPU: Процессоры редко простаивают, пока есть работа.
  • Низкие накладные расходы: В большинстве случаев P работает со своей локальной очередью, что очень быстро. "Воровство" происходит только при необходимости.

Таким образом, горутины не "берут задачи" друг у друга напрямую. Это делает за них планировщик с помощью механизма work-stealing, автоматически и эффективно балансируя нагрузку по всем доступным процессорным ядрам.