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

Ответ

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

  • G (Goroutine): Легковесный поток, управляемый Go. Имеет небольшой стек (от 2KB), который может расти по мере необходимости.
  • M (Machine): Системный поток ОС. Непосредственно выполняет код.
  • P (Processor): Контекст для выполнения. У каждого P есть локальная очередь горутин. Количество P по умолчанию равно количеству ядер ЦП (GOMAXPROCS).

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

  1. Планировщик привязывает M к P.
  2. M начинает выполнять горутины из очереди P.
  3. Если горутина блокируется (например, на системном вызове), M отсоединяется от P, а рантайм может создать новый M для этого P, чтобы продолжить выполнение других горутин.
  4. Если у одного P заканчиваются горутины, он может "украсть" их у другого P (механизм work-stealing) для балансировки нагрузки.
package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        // Эта горутина будет поставлена в очередь
        // и выполнена планировщиком на доступном потоке.
        fmt.Println("Горутина выполняется")
    }()

    // Даем планировщику время на запуск горутины.
    // Без этого main может завершиться раньше.
    time.Sleep(time.Millisecond)
}

Таким образом, горутины не привязаны жестко к одному потоку ОС, что делает их очень легковесными и позволяет создавать сотни тысяч и даже миллионы горутин в одном приложении.