Что такое планировщик в Go (Go Scheduler) и как он работает?

Ответ

Планировщик Go (Go Scheduler) — это компонент рантайма Go, который отвечает за распределение горутин (goroutines) по рабочим потокам операционной системы (OS threads) для их выполнения.

Его главная задача — эффективно утилизировать процессорное время, управляя сотнями тысяч горутин на небольшом количестве системных потоков.

Ключевые компоненты (модель G-P-M):

  • G (Goroutine): Легковесный поток, управляемый Go. Имеет свой стек, который может расти и сжиматься. Это то, что вы создаете с помощью ключевого слова go.
  • M (Machine): Поток операционной системы (OS thread). Это реальный поток, который выполняет код.
  • P (Processor): Контекст для выполнения горутин. У каждого P есть локальная очередь горутин, готовых к выполнению. Количество P по умолчанию равно количеству ядер CPU (runtime.GOMAXPROCS).

Основные принципы работы:


  1. Модель M:N: Планировщик отображает M горутин на N потоков ОС. Обычно N невелико (равно GOMAXPROCS), а M может быть огромным.



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


  3. Кооперативное вытеснение (Cooperative Preemption):

    • Горутина добровольно уступает процессор при выполнении блокирующих операций (системные вызовы, работа с каналами, мьютексами).
    • Начиная с Go 1.14, планировщик стал вытесняющим. Если горутина выполняется слишком долго без вызовов функций (например, в плотном цикле), рантайм может принудительно остановить её и поставить в очередь, дав поработать другим горутинам. Это решает проблему "зависания" планировшика на одной долгой задаче.
func main() {
    // Запускаем две горутины
    go func() {
        fmt.Println("Goroutine 1")
    }()
    go func() {
        fmt.Println("Goroutine 2")
    }()

    // runtime.Gosched() явно просит планировщик 
    // уступить процессор другим горутинам.
    // Без этого вызова main может завершиться раньше,
    // чем другие горутины успеют выполниться.
    runtime.Gosched()
}