Что такое runtime (планировщик sheduler)? Как он устроен в Go?go-58

Что такое runtime в Go?

Runtime в Go — это набор компонентов, выполняющихся вместе с пользовательским кодом и обеспечивающих:

  • Управление горутинами (создание, планирование, уничтожение)
  • Управление памятью (аллокация, сборка мусора)
  • Сетевые поллинговые операции
  • Системные вызовы
  • И другие низкоуровневые функции

Планировщик в деталях

Основные концепции

  1. M (Machine) — поток ОС (kernel thread)
  2. G (Goroutine) — легковесный поток Go
  3. P (Processor) — логический процессор (до Go 1.1 был только M и G)
// Упрощенная структура из runtime/runtime2.go
type g struct {     // Goroutine
    stack stack   // Стек горутины
    sched gobuf   // Контекст выполнения
}

type m struct {     // OS thread
    g0      *g     // Специальная горутина для выполнения runtime-кода
    curg    *g     // Текущая выполняемая горутина
    p       puintptr // Привязанный P
}

type p struct {     // Logical processor
    runqhead uint32 // Очередь runnable-горутин
    runqtail uint32
    runq     [256]guintptr
}

Принцип работы

  1. Трехкомпонентная модель (M:P:G):

    • M (поток ОС) должен быть привязан к P для выполнения Go-кода
    • P управляет набором G (горутин)
    • Количество P обычно равно GOMAXPROCS (по умолчанию — количество CPU ядер)
  2. Work-stealing алгоритм:

    • Если у P нет готовых к выполнению горутин, он пытается "украсть" половину очереди у другого P
  3. Системные вызовы:

    • При блокирующем syscall, P отвязывается от M и может быть использован другим M
    • После завершения syscall, горутина пытается получить P для продолжения работы

Основные механизмы

1. Переключение контекста

Планировщик переключает горутины в случаях:

  • Канальные операции (send/receive)
  • Системные вызовы
  • Сетевые операции
  • Вызов runtime.Gosched()
  • Сборка мусора

2. Sysmon

Отдельный M, выполняющий фоновые задачи:

  • Проверка deadlock'ов
  • Принудительное переключение долго выполняющихся G
  • Сетевой поллинг
  • Освобождение памяти

Преимущества модели Go

  1. Эффективность:

    • Переключение горутин дешевле переключения потоков ОС (200ns vs 1000ns)
    • Нет необходимости в большом стеке (начинается с 2KB)
  2. Масштабируемость:

    • Миллионы горутин на одном хосте
    • Автоматическое распределение по CPU ядрам
  3. Простота:

    • Абстракция над сложностью работы с потоками ОС

Пример работы планировщика

func main() {
    runtime.GOMAXPROCS(2) // 2 логических процессора

    var wg sync.WaitGroup
    wg.Add(3)

    go func() { // G1
        defer wg.Done()
        fmt.Println("Горутина 1")
    }()

    go func() { // G2
        defer wg.Done()
        time.Sleep(time.Second) // Вызовет перепланирование
        fmt.Println("Горутина 2")
    }()

    go func() { // G3
        defer wg.Done()
        fmt.Println("Горутина 3")
    }()

    wg.Wait()
}

Настройки планировщика

  1. GOMAXPROCS — максимальное количество P
  2. GODEBUG=schedtrace=1000 — логирование работы планировщика (каждые 1000ms)
  3. runtime.LockOSThread() — привязка горутины к текущему M

Под капотом: состояния горутин

  1. Runnable — готова к выполнению, ждет P
  2. Executing — выполняется на P
  3. Syscall — выполняет блокирующий системный вызов
  4. Waiting — заблокирована (канал, таймер и т.д.)

Резюмируем

планировщик Go — это sophisticated механизм, обеспечивающий эффективное выполнение миллионов легковесных горутин на ограниченном количестве потоков ОС. Его трехуровневая архитектура (M:P:G) и work-stealing алгоритмы делают Go-программы высокопроизводительными и масштабируемыми без сложного ручного управления потоками.