← Назад к вопросам

Как устроен стейт планировщика?

1.6 Junior🔥 141 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Устройство стейта планировщика Go

Стейт планировщика Go (runtime.sched) — это глобальная структура данных, которая управляет всеми аспектами планирования горутин в рантайме Go. Это ядро многопоточности языка, отвечающее за распределение горутин по потокам операционной системы (OS threads).

Основные компоненты стейта планировщика

Глобальный стейт планировщика определяется в runtime/runtime2.go как структура schedt:

type schedt struct {
    // Глобальная очередь готовых к выполнению горутин
    runq     gQueue
    runqsize int32
    
    // Свободные горутины (пул для переиспользования)
    gFree struct {
        list    gList
        n       int32
    }
    
    // Свободные M (потоки ОС)
    midle      muintptr
    nmidle     int32
    nmidlelocked int32
    
    // Свободные P (процессоры)
    pidle      puintptr
    npidle     uint32
    
    // Глобальный счетчик последнего использованного P
    schedlastupdate int64
    goidgen      uint64
    
    // Статистика и мониторинг
    nmspinning uint32
    nmsys      uint32
    // ... другие поля
}

Ключевые сущности в стейте планировщика

  1. G (goroutine) — представляют сами горутины
  2. M (machine) — потоки операционной системы
  3. P (processor) — виртуальные процессоры (до Go 1.1 не существовали)

Основные очереди и структуры данных

Глобальная очередь (runq)

// Глобальная очередь горутин, готовых к выполнению
var runq gQueue

Когда локальные очереди P переполняются (более 256 горутин), излишки перемещаются в глобальную очередь.

Локальные очереди P

Каждый P имеет свою локальную очередь горутин:

type p struct {
    // Локальная очередь горутин
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr
    
    // Ссылка на следующую горутину для выполнения
    runnext guintptr
    // ... другие поля
}

Состояния планировщика

Планировщик отслеживает несколько ключевых состояний:

Состояния M (потоков ОС)

  • Running — выполняет код
  • Spinning — ищет работу (активный ожидание)
  • Sleeping — заблокирован или ожидает работы
  • Syscall — выполняет системный вызов

Состояния P (процессоров)

  • Idle — свободен, ожидает работы
  • Running — привязан к M и выполняет код
  • Syscall — связан с M в системном вызове
  • Dead — остановлен

Алгоритм работы планировщика

  1. Work-stealing алгоритм:
    • Если у P пустая локальная очередь, он пытается "украсть" горутины:
     - Из глобальной очереди
     - Из других переполненных локальных очередей
     - Из сети поллинга (network poller)

  1. Балансировка нагрузки:
    // Функция балансировки вызывается периодически
    func schedule() {
        gp := getg()
        
        // 1. Проверяем runnext (горутина с высшим приоритетом)
        // 2. Проверяем локальную очередь
        // 3. Пытаемся украсть у других P
        // 4. Проверяем глобальную очередь
        // 5. Проверяем network poller
        // 6. Если работы нет, переводим M в спящий режим
    }
    

Синхронизация и блокировки

Доступ к стейту планировщика защищен несколькими блокировками:

  • sched.lock — защищает глобальные структуры планировщика
  • p.lock — защищает отдельные P
  • m.locks — счетчик блокировок для M
// Пример захвата блокировки планировщика
func lockSched() {
    lock(&sched.lock)
}

func unlockSched() {
    unlock(&sched.lock)
}

Инициализация планировщика

При старте программы:

  1. Инициализируется глобальная структура sched
  2. Создаются начальные P (по количеству GOMAXPROCS)
  3. Запускается начальная горутина main
  4. Запускаются системные мониторы и сборщик мусора
func schedinit() {
    // Инициализация глобального планировщика
    sched.maxmcount = 10000
    sched.nmidlelocked = 0
    sched.nmsys = 0
    sched.nmfreed = 0
    
    // Создание P
    for i := 0; i < procs; i++ {
        newP := allp[i]
        if newP == nil {
            newP = new(p)
        }
        // Инициализация P
    }
}

Особенности реализации

  1. Локальность кэша — P хранят локальные очереди для лучшей кэш-локальности
  2. Активное ожидание (spinning) — M могут активно искать работу, уменьшая latency
  3. Hand-off механизм — при блокировке в syscall, P может быть передан другому M
  4. Предвыбор (preemption) — горутины могут быть принудительно вытеснены

Мониторинг и отладка

Состояние планировщика можно отслеживать через:

  • GODEBUG=schedtrace=1000 — трассировка планировщика
  • runtime.ReadMemStats — статистика памяти планировщика
  • debug.PrintStack() — дамп стека планировщика

Стейт планировщика Go — это сложная, но эффективно организованная система, обеспечивающая максимальную утилизацию CPU при минимальных накладных расходах на переключение контекста. Его архитектура с разделением на G, M и P позволяет масштабироваться на десятки тысяч горутин с минимальными блокировками и эффективным распределением работы.

Как устроен стейт планировщика? | PrepBro