Как устроен планировщик (scheduler) в Go? Расскажите про модель M, P, G.

Ответ

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


  • G (Goroutine): Легковесный поток, управляемый рантаймом Go. Имеет свой собственный стек, который может расти и сжиматься. Это то, что вы создаете с помощью ключевого слова go.



  • M (Machine): Системный поток (OS Thread), управляемый операционной системой. Это реальный исполнитель кода.



  • P (Processor): Контекст для выполнения. Это ключевая абстракция, которая связывает горутины с потоками. У каждого P есть локальная очередь исполняемых горутин. Количество P по умолчанию равно количеству ядер CPU и контролируется переменной runtime.GOMAXPROCS.


Как это работает:

  1. Для выполнения P должен быть привязан к M.
  2. P берет G из своей локальной очереди и выполняет ее на M.
  3. Work Stealing (Воровство работы): Если у одного P заканчиваются горутины в очереди, он может "украсть" половину горутин из очереди другого P, чтобы сбалансировать нагрузку и не простаивать.
  4. Блокирующие системные вызовы: Если G на M выполняет блокирующий системный вызов (например, чтение файла), P отсоединяется от этого M (вместе с заблокированной G), находит другой свободный M (или создает новый) и продолжает выполнять другие горутины из своей очереди. Это предотвращает блокировку целого потока ОС.
func main() {
    // Устанавливаем количество "процессоров" (P), которые будут
    // одновременно выполнять горутины.
    runtime.GOMAXPROCS(2)

    var wg sync.WaitGroup
    wg.Add(10)

    for i := 0; i < 10; i++ {
        go func(i int) {
            defer wg.Done()
            fmt.Printf("Горутина %d выполняетсяn", i)
            // какая-то работа
            time.Sleep(10 * time.Millisecond)
        }(i)
    }

    wg.Wait()
}

Эта модель позволяет Go достигать высокой степени параллелизма с минимальными накладными расходами на переключение контекста по сравнению с традиционными потоками ОС.