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

Как горутина попадает на нитку вычислений процессора?

3.0 Senior🔥 191 комментариев
#Конкурентность и горутины#Основы Go#Производительность и оптимизация

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

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

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

Как горутина попадает на поток (нитку) ОС

Чтобы понять механизм планирования горутин, нужно разобраться в двухуровневой модели планирования Go: планировщик Go (Goroutine Scheduler) работает поверх планировщика потоков операционной системы.

1. Архитектура планировщика Go: модель M-P-G

Планировщик Go построен на трёх ключевых абстракциях:

  • G (Goroutine) — сама горутина, её состояние и стек.
  • M (Machine) — поток ОС (Machine Thread, "нить"), который непосредственно выполняется на ядре процессора.
  • P (Processor) — виртуальный процессор (контекст планировщика), который связывает M и G. P содержит очередь готовых к выполнению горутин (runqueue).
// Упрощённое представление структур в runtime (псевдокод)
type g struct {
    stack     stack   // стек горутины
    sched     gobuf   // контекст исполнения (регистры)
    status    uint32  // статус: running, runnable, waiting и т.д.
}

type p struct {
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr  // локальная очередь из 256 горутин
    m        muintptr       // привязанный поток M (или nil)
}

type m struct {
    g0      *g        // системная горутина
    curg    *g        // текущая выполняемая горутина
    p       puintptr  // привязанный P
    thread  uintptr   // дескриптор потока ОС
}

2. Путь горутины на физический CPU

Процесс можно разбить на следующие этапы:

Этап 1: Создание и постановка в очередь

Когда вы запускаете горутину через go func() {...}, происходит:

  • Аллокация структуры G и её стека.
  • Настройка точки входа (функции).
  • Горутина помещается в локальную очередь (runqueue) текущего P (виртуального процессора) исполняющей её горутины.
// Пример запуска горутины
go func() {
    fmt.Println("Hello from goroutine")
}()

Этап 2: Выбор горутины для исполнения

Каждый P старается выполнять свои горутин из локальной очереди. Это work-stealing планировщик:

  1. P берёт следующую G из своей локальной очереди (runqueue).
  2. Если очередь пуста, P пытается "украсть" (steal) половину горутин из очереди другого P.
  3. Если нечего украсть — проверяет глобальную очередь (используется реже).

Этап 3: Привязка к потоку ОС (M)

  • P должен быть привязан к потоку ОС (M), чтобы исполнять код.
  • Если у P нет привязанного M (например, все потоки заблокированы), планировщик может создать новый поток ОС через системный вызов (например, clone() в Linux).
  • M запрашивается из пула или создаётся, затем привязывается к P.

Этап 4: Непосредственное исполнение на CPU

  • M — это поток ОС (pthread/thread в Windows). Только ОС решает, на каком физическом ядре CPU будет выполняться этот поток.
  • Когда планировщик ОС выделяет M квант времени на ядре:
    - `M` выполняет машинный код, сгенерированный из инструкций горутины `G`.
    - `M` через `P` продолжает выбирать и выполнять следующие горутины, если текущая завершилась или заблокировалась.

3. Ключевые моменты переключения контекста

  • Кооперативная многозадачность между горутинами: Go-планировщик сам переключает горутины в критических точках (вызов канала, системный вызов, go, runtime.Gosched()). Это не вытесняющее планирование на уровне горутин.
  • Вытесняющее планирование на уровне потоков ОС: Поток M может быть вытеснен ОС в любой момент согласно политике планировщика ОС (round-robin и др.).
  • Блокирующие операции: Если горутина выполняет блокирующий системный вызов (например, файловый ввод-вывод), планировщик Go может отделить P от M:
    1. `P` освобождается и может быть использован другим `M`.
    2. Создаётся новый поток ОС (`M`), если нужно, чтобы обслуживать другие горутины на этом `P`.
    3. Когда системный вызов завершается, горутина возвращается в очередь.

4. Роль runtime и сет-полиллера (с версии Go 1.14)

Начиная с Go 1.14, реализована асинхронная вытесняющая многозадачность:

  • Сет-полиллер (netpoller) — интегрирован в планировщик для асинхронного ввода-вывода.
  • Сигналы вытеснения: Планировщик использует сигналы OS (например, SIGURG в Unix) для прерывания длительно исполняющихся горутин, чтобы дать ход другим.

Итог: путь от создания до CPU

  1. go → создаётся G → помещается в очередь P.
  2. P (с привязанным M) выбирает G из своей очереди (или ворует у других).
  3. M (поток ОС) выполняет машинный код G на физическом ядре CPU согласно планировщику ОС.
  4. При блокировке или вытеснении M может освободить P, который затем обслуживает другие горутины через другие потоки M.

Эта модель обеспечивает высокую эффективность: миллионы горутин могут обслуживаться небольшим фиксированным числом потоков ОС, а быстрые переключения контекста происходят в пространстве пользователя без дорогостоящих системных вызовов.