Как горутина оказывается в глобальной очереди?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как горутина попадает в глобальную очередь планировщика 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)
}
}
}
Почему важна глобальная очередь
- Балансировка нагрузки: GRQ выступает как буфер для перераспределения задач между процессорами.
- Изоляция: Локальные очереди уменьшают contention, но GRQ обеспечивает "запасной" путь, когда локальные очереди пусты.
- Гибкость: Горутины в GRQ могут быть захвачены любым свободным M, что повышает утилизацию ресурсов.
Производительность и особенности
- Доступ к GRQ медленнее, чем к LRQ, из-за необходимости синхронизации между потоками.
- Планировщик старается минимизировать использование GRQ, предпочитая локальные очереди.
- В современных версиях Go (1.14+) улучшена работа с GRQ за счёт более агрессивной преемпции и улучшенного балансировщика.
Таким образом, горутина оказывается в глобальной очереди в основном при переполнении локальных очередей, возврате из системных вызовов или в результате балансировки нагрузки. Это механизм, обеспечивающий глобальную справедливость и эффективное использование ресурсов в многопоточном окружении.