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

Что такое глобальная очередь?

3.0 Senior🔥 22 комментариев
#Конкурентность и горутины

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

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

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

Что такое глобальная очередь в контексте Go

В языке программирования Go глобальная очередь (англ. global run queue) — это один из ключевых компонентов планировщика горутин (goroutine scheduler). Это структура данных, которая хранит готовые к выполнению горутины, не привязанные к какому-либо конкретному потоку операционной системы (OS thread). Планировщик Go использует её как часть модели работы с очередями M:P:G.

Как устроена модель планировщика Go

Чтобы понять роль глобальной очереди, нужно разобраться в абстракциях планировщика:

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

Каждый P имеет свою локальную очередь (local run queue) — кольцевой буфер, в котором хранятся горутины, готовые к выполнению именно на этом P. Глобальная очередь — это общая для всех P очередь, реализованная как связанный список. Она выступает в роли балансировщика нагрузки между всеми виртуальными процессорами.

Назначение и принцип работы глобальной очереди

Глобальная очередь решает несколько важных задач:

  1. Балансировка нагрузки между процессорами (P). Если у одного P в локальной очереди скопилось много горутин, а другой P простаивает, планировщик может переложить часть горутин из локальной очереди в глобальную. Оттуда их сможет забрать любой свободный P.
  2. Приём вновь созданных горутин. Когда создаётся новая горутина (например, через go func()), она сначала помещается в локальную очередь P, которому принадлежит горутина-родитель. Однако если локальная очередь переполнена (обычно её размер — 256 горутин), половина горутин из неё перемещается в глобальную очередь.
  3. Поиск работы для свободного P. Когда у P заканчиваются горутины в локальной очереди, он выполняет так называемый work-stealing:
    *   Сначала проверяет локальную очередь.
    *   Затем пытается **украсть** (steal) несколько горутин из локальных очередей других P.
    *   И только если это не удалось, обращается к **глобальной очереди**.

    Обратите внимание: доступ к глобальной очереди является **менее приоритетным**, чем воровство из локальных очередей других P, так как требует глобальной блокировки (через `mutex`) и поэтому дороже.

Пример на практике

Рассмотрим упрощённый пример, где видна роль глобальной очереди:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // Увеличим количество создаваемых горутин
    runtime.GOMAXPROCS(2) // Установим 2 виртуальных процессора (P)

    for i := 0; i < 10; i++ {
        go func(id int) {
            fmt.Printf("Горутина %d запущена на P\n", id)
            time.Sleep(10 * time.Millisecond) // Симуляция работы
        }(i)
    }

    time.Sleep(time.Second) // Дадим время на выполнение
}

В этом примере:

  1. Создаются 10 горутин.
  2. Изначально они помещаются в локальные очереди тех P, на которых работает главная горутина main и, возможно, первые запущенные дочерние горутины.
  3. Если локальная очередь какого-либо P переполнится, часть горутин будет сброшена в глобальную очередь.
  4. Свободный или малозагруженный P (например, второй, если первый занят) будет периодически проверять глобальную очередь и забирать оттуда задачи для выполнения.

Важные особенности и отличия от локальной очереди

  • Синхронизация: Доступ к глобальной очереди защищён мьютексом планировщика (sched.lock), что делает операции push и pop на ней более дорогими по сравнению с lock-free-операциями над локальной очередью (локальная очередь использует атомарные операции для головы и хвоста).
  • Стратегия выборки: Планировщик старается работать с локальными очередями, чтобы сохранить локальность данных (горутины, созданные вместе, часто работают с общими данными, что хорошо для кеша CPU). Обращение к глобальной очереди — это крайняя мера.
  • Реализация: Глобальная очередь (sched.runq) — это обычный связанный список, в то время как локальная очередь — это эффективный кольцевой буфер (массив фиксированного размера на 256 элементов).

Заключение

Таким образом, глобальная очередь — это критически важный механизм балансировки и распределения работы в рантайме Go. Она обеспечивает:

  • Справедливость — не даёт одному P надолго захватить все задачи.
  • Устойчивость к перегрузкам — принимает "излишки" горутин при всплесках создания.
  • Эффективное использование ресурсов — позволяет простаивающим потокам M и процессорам P находить работу.

Понимание работы глобальной и локальных очередей помогает писать более эффективный конкуррентный код, так как объясняет, почему, например, создание огромного количества горутин "одним пакетом" может привести к накладным расходам на синхронизацию и балансировку между ядрами CPU.

Что такое глобальная очередь? | PrepBro