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

Как горутина оказывается в глобальной очереди?

2.7 Senior🔥 151 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Как горутина попадает в глобальную очередь планировщика Go

Чтобы понять, как горутина оказывается в глобальной очереди (Global Run Queue, GRQ), нужно разобраться в архитектуре планировщика Go (GMP-модель) и его алгоритмах распределения работы. Глобальная очередь — это один из ключевых компонентов планировщика, предназначенный для хранения горутин, которые ещё не привязаны к конкретному потоку ОС (M) или локальной очереди P.

Основные причины попадания горутины в GRQ

1. Создание новой горутины с помощью go func()

При запуске новой горутины, планировщик сначала пытается поместить её в локальную очередь (LRQ) текущего P (процессора). Однако, если LRQ переполнена (обычно вмещает до 256 горутин), половина задач из локальной очереди перемещается в глобальную, а новая горутина добавляется в LRQ. В некоторых случаях (например, при старте) горутина может сразу попасть в GRQ.

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 1000; i++ {
        // При массовом создании горутин часть может оказаться в GRQ
        go func(id int) {
            fmt.Printf("Горутина %d\n", id)
        }(i)
    }
    time.Sleep(time.Second)
}

2. Завершение системного вызова

Когда горутина возвращается из блокирующего системного вызова (например, операций ввода-вывода), она не всегда может сразу вернуться к своему старому P. В таком случае планировщик помещает её в глобальную очередь, чтобы любой свободный M мог её захватить.

package main

import (
    "net/http"
    "time"
)

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            // Системный вызов (сетевое IO)
            resp, err := http.Get("https://example.com")
            if err == nil {
                resp.Body.Close()
            }
            // После возврата из syscall горутина может попасть в GRQ
        }()
    }
    time.Sleep(time.Second * 2)
}

3. Балансировка нагрузки между процессорами (P)

Планировщик периодически выполняет work-stealing и load-balancing. Если у одного P перегружена локальная очередь, а у других есть свободные мощности, часть горутин может быть перемещена в глобальную очередь для последующего перераспределения. Также при work-stealing, если у P нет задач в LRQ и глобальной очереди, он может "украсть" задачи из LRQ другого P через GRQ как промежуточный этап.

4. Вызов runtime.Gosched()

Явное указание планировщику перепланировать горутину может привести к её перемещению в глобальную очередь.

package main

import (
    "runtime"
    "time"
)

func main() {
    go func() {
        for i := 0; i < 5; i++ {
            println("Уступаем выполнение")
            runtime.Gosched() // Текущая горутина может быть помещена в GRQ
            time.Sleep(time.Millisecond * 100)
        }
    }()
    time.Sleep(time.Second)
}

Механизм работы с глобальной очередью

Глобальная очередь реализована как lock-free структура данных (используются атомарные операции), чтобы минимизировать contention при доступе из разных потоков ОС (M). Основные операции:

  • Добавление: При помещении горутины в GRQ используется атомарная операция CAS (Compare-And-Swap).
  • Извлечение: Когда P ищет работу, он сначала проверяет свою LRQ, затем GRQ, и только потом пытается steal из других P.
// Псевдокод логики планировщика
func schedule() {
    for {
        // 1. Проверить локальную очередь
        if gp := runqget(localP); gp != nil {
            execute(gp)
        }
        
        // 2. Проверить глобальную очередь
        if gp := runqget(globalQueue); gp != nil {
            execute(gp)
        }
        
        // 3. Попытаться украсть задачу у другого P
        if gp := stealFromOtherP(); gp != nil {
            execute(gp)
        }
    }
}

Почему важна глобальная очередь

  1. Балансировка нагрузки: GRQ выступает как буфер для перераспределения задач между процессорами.
  2. Изоляция: Локальные очереди уменьшают contention, но GRQ обеспечивает "запасной" путь, когда локальные очереди пусты.
  3. Гибкость: Горутины в GRQ могут быть захвачены любым свободным M, что повышает утилизацию ресурсов.

Производительность и особенности

  • Доступ к GRQ медленнее, чем к LRQ, из-за необходимости синхронизации между потоками.
  • Планировщик старается минимизировать использование GRQ, предпочитая локальные очереди.
  • В современных версиях Go (1.14+) улучшена работа с GRQ за счёт более агрессивной преемпции и улучшенного балансировщика.

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