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

В каких ситуациях горутины попадают в локальную очередь

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

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

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

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

Локальные очереди горутин в Go

Горутины в Go могут попадать в локальные очереди (local runqueue) планировщика в нескольких специфических ситуациях. Это важный механизм для эффективного распараллеливания и минимизации накладных расходов на синхронизацию. Локальные очереди связаны с процессорами (P) в модели планировщика Go (GMP), где:

  • G - горутина (goroutine)
  • M - поток ОС (machine)
  • P - процессор (processor), логический контейнер ресурсов для исполнения G

Ключевые ситуации попадания горутин в локальную очередь

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

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

func main() {
    // Эта горутина будет помещена в локальную очередь P, выполняющего main()
    go func() {
        fmt.Println("Новая горутина")
    }()
}

Механизм: когда выполняется go func()..., планировщик:

  • Создает новую структуру G
  • Проверяет текущий P (который связан с текущим M)
  • Если локальная очередь P не переполнена (обычно ограничена 256 горутин), помещает G в локальную очередь этого P
  • Это предотвращает конкуренцию на глобальной очереди и уменьшает блокировки

2. Возвращение горутины из блокированного состояния

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

func worker(ch chan struct{}) {
    <-ch // Блокировка на чтении из канала
    // После получения данных горутина вернётся в локальную очередь P
    fmt.Println("Продолжение работы")
}

3. Перепланирование при системных вызовах

При выполнении неблокирующих системных вызовов (например, некоторые операции с файлами через epoll в Linux) горутина может быть временно возвращена в локальную очередь вместо полного освобождения M.

4. Работа планировщика при балансировке нагрузки

Планировщик периодически проверяет распределение горутин между P и может перемещать горутины из глобальной очереди в локальные очереди менее загруженных P. Это происходит:

  • При недостатке горутин в локальной очереди текущего P
  • Во время регулярных проверок планировщика (каждые 10ms или при определенных событиях)
// Пример, где планировщик может балансировать горутины между P
for i := 0; i < 1000; i++ {
    go func(id int) {
        // Эти 1000 горутин будут распределены между локальными очередями всех P
        time.Sleep(time.Millisecond)
    }(i)
}

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

  • Снижение конкуренции: Локальные очереди уменьшают необходимость синхронизации через мьютексы при доступе к глобальной очереди
  • Локализация данных: Горутины, созданные в одном контексте (например, в одном потоке обработки запросов), чаще выполняются на том же P, что может улучшить использование кэша CPU
  • Эффективность планирования: Быстрый доступ к локальной очереди без блокировок

Ограничения и исключения

Горутина НЕ попадет в локальную очередь в следующих случаях:

  1. Переполнение локальной очереди: Если локальная очередь текущего P уже содержит 256 горутин, новые попадают в глобальную очередь
  2. Отсутствие доступного P: Если горутина запускается когда текущий P отсутствует (например, в некоторых callback из C)
  3. Принудительное помещение в глобальную очередь: Планировщик может специально поместить горутину в глобальную очередь для балансировки нагрузки
  4. Горутины, связанные с сетевым I/O: При использовании netpoll (асинхронного сетевого I/O) горутины могут управляться отдельным механизмом
// Пример переполнения локальной очереди
func main() {
    for i := 0; i < 300; i++ {
        go func() {
            time.Sleep(time.Hour) // Каждая горутина блокируется
        }()
        // После ~256 горутин новые будут попадать в глобальную очередь
    }
}

Внутренняя реализация (упрощённо)

В исходном коде Go (runtime/proc.go) логика примерно такая:

// Упрощённая иллюстрация логики (не реальный код)
func newGoroutine() {
    gp := createG() // Создать структуру G
    p := getCurrentP() // Получить текущий P
    
    if p != nil && p.localQueueSize < 256 {
        p.localQueue.push(gp) // В локальную очередь
    } else {
        globalQueue.push(gp) // В глобальную очередь
    }
}

Практические следствия для разработчика

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

Локальные очереди — важный оптимизирующий механизм планировщика Go, который уменьшает накладные расходы на синхронизацию и улучшает производительность параллельных программ.

В каких ситуациях горутины попадают в локальную очередь | PrepBro