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

Как процессор взаимодействует с горутинами?

2.2 Middle🔥 231 комментариев
#Конкурентность и горутины

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

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

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

Взаимодействие процессора с горутинами в Go

Взаимодействие процессора с горутинами в Go — это результат сложной работы планировщика Go (scheduler) и глубокой интеграции с системным планировщиком операционной системы. Этот процесс построен на принципах M:N-планирования, где M горутин (пользовательских потоков) распределяются по N потокам операционной системы (обычно это системные потоки, POSIX-потоки в Linux), которые, в свою очередь, выполняются на доступных ядрах процессора.

Абстрактная модель планировщика: G, M, P

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

  • G (Goroutine) — представляет собой горутину, содержащую ее состояние (стек, указатель инструкции) и контекст.
  • M (Machine) — представляет собой системный поток (OS thread). Именно M выполняется на ядре процессора.
  • P (Processor) — представляет собой контекст планировщика для Go. P связывает M с ресурсами (локальной очередью горутин) для их выполнения. Количество P обычно равно количеству логических ядер процессора (GOMAXPROCS).

Механизм взаимодействия процессора с горутиной

Рассмотрим детальный путь от горутины до инструкций процессора.

  1. Горутина (G) создается и помещается в очередь. Когда вы выполняете go func() {...}, планировщик создает структуру G и помещает ее в локальную очередь одного из P.
go func() {
    fmt.Println("Горутина запущена")
}()
  1. Системный поток (M) связывается с P. Свободный или созданный системный поток M "привязывается" к доступному P. Этот P предоставляет M ресурсы для выполнения работы.

  2. P предоставляет горутину для выполнения. M, связанный с P, берет следующую готовую горутину G из локальной очереди P (или из глобальной очереди, если локальная пуста) и начинает ее выполнение. Стек горутины и указатель инструкции загружаются в контекст M.

  3. Ядро процессора выполняет системный поток (M). Операционная система планирует системный поток M для выполнения на одном из физических или логических ядер процессора. Процессор не имеет прямой концепции "горутины" — он выполняет поток инструкций из системного потока. Таким образом, горутина получает время на процессоре через системный поток M, который ее "несет".

// В этом месте код горутины фактически исполняется на ядре процессора.
fmt.Println("Горутина запущена")

Ключевые моменты взаимодействия при выполнении

Процессор взаимодействует с горутинами не напрямую, а через следующие динамические процессы, управляемые планировщиком:

  • Превентивное планирование. Планировщик Go не дает одной горутине монополизировать процессор. Если горутина выполняется слишком долго (примерно >10ms), планировщик может прервать ее выполнение. Для этого он использует сигналы от операционной системы (например, SIGURG в Linux) или проверки на определенных точках (например, при вызове функции). После прерывания текущая M "откладывает" текущую G обратно в очередь и берет следующую для выполнения.

  • Блокирующие операции и системные вызовы. Когда горутина выполняет блокирующую операцию (например, чтение файла, сетевой запрос, вызов time.Sleep), происходит важное разделение:

    *   Если блокирующая операция может быть выполнена быстро (например, канал с доступными данными), планировщик может просто переключить контекст внутри того же M/P.
    *   Если операция требует длительного ожидания системного вызова (например, чтение из сети), текущая **M отсоединяется от P** и блокируется вместе с горутиной в ожидании ответа от ОС. Освободившийся **P немедленно связывается с другой свободной M** (или создает новую) и продолжает выполнять другие горутины из своей очереди. Это позволяет эффективно использовать процессор, не простаивая в ожидании. Когда системный вызов завершается, M пытается найти свободный P чтобы продолжить выполнение своей G.

// Пример блокирующей операции
data := make([]byte, 100)
n, err := file.Read(data) // M может отсоединиться от P на время этого системного вызова
  • Перепланирование при синхронизации. Операции на каналах (chan) или мьютексах (sync.Mutex) часто приводят к тому, что горутина переходит в состояние ожидания. Планировщик перекладывает ожидающую G в соответствующую очередь и немедленно запускает другую G на текущем M/P, чтобы процессор не простаивал.

Преимущества такой модели для использования процессора

Эта сложная абстракция позволяет Go достичь исключительно высокой эффективности использования процессора:

  1. Минимальные затраты на переключение контекста. Переключение между горутинами внутри одного M/P происходит очень быстро, так как это простые манипуляции с указателями в памяти пользовательского пространства, без затратного системного вызова для переключения потоков ОС.
  2. Обеспечение параллельности на многоядерных системах. Каждый P может быть связан с отдельным M, который выполняется на отдельном ядре процессора. Это позволяет сотням тысяч горутин распределяться по всем ядрам и выполняться параллельно.
  3. Асинхронная обработка блокирующих вызовов. Модель отсоединения M от P при длительных системных вызовах гарантирует, что горутины, не требующие ожидания (например, выполняющие вычисления), могут продолжать использовать процессор, не блокируясь на одной операции I/O.

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