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

Как действует поток, если у него опустела очередь?

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

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

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

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

Механизм работы потока M при пустой локальной очереди (runq)

В Go runtime, когда у потока операционной системы (M), выполняющего горутины, опустела его локальная очередь исполнения (local runqueue), он не блокируется и не завершается. Вместо этого он активирует сложный алгоритм ворк-стилинга (work stealing), чтобы найти новую работу и максимально загрузить ядра CPU. Этот механизм — ключевой элемент эффективного планировщика Go.

Последовательность действий потока M при исчерпании локальной очереди

  1. Проверка глобальной очереди (global runqueue):
    Поток M сначала пытается забрать пакет горутин из **глобальной очереди**. Эта очередь защищена глобальным мьютексом (`sched.lock`), доступ к которому — потенциально дорогостоящая операция. Поэтому M пытается забрать сразу до `len(local_runqueue)/2 + 1` горутин, чтобы минимизировать частоту обращения к глобальной очереди.
```go
// Псевдокод, иллюстрирующий логику
if batch := stealFromGlobalQueue(); batch != nil {
    pushToLocalQueue(batch)
    resumeExecution()
    return
}
```

2. Ворк-стилинг (work stealing) у других потоков:

    Если глобальная очередь пуста, M переходит к ворк-стилингу. Он случайным образом выбирает другой поток-сосед (**P** — процессор, который держит связку M+G+локальная очередь) и пытается "украсть" половину горутин из его *локальной* очереди.
```go
// Примерная логика ворк-стилинга
victimP := selectRandomOtherP()
stolenGoroutines := stealHalfOfLocalRunq(victimP)
if stolenGoroutines > 0 {
    startExecuting(stolenGoroutines[0])
    pushToLocalQueue(stolenGoroutines[1:])
    return
}
```
    Это уравновешивает нагрузку между потоками: занятые потоки делятся работой с простаивающими.

  1. Опрос сети (network poller):
    Если ворк-стилинг не дал результатов, M проверяет **network poller** — компонент рантайма, который уведомляет о готовности сетевых операций ввода-вывода (например, сокетов). Если есть горутины, которые ожидали I/O и теперь данные готовы, M забирает их на выполнение.
```go
if gList := netpoller.FindReadyGoroutines(); gList != nil {
    injectReadyGoroutinesToLocalQueue(gList)
    resumeExecution()
    return
}
```

4. Поиск горутин в мьютексах планировщика:

    M также проверяет, нет ли горутин, разблокированных из-за операций с **sync.Mutex**, **sync.RWMutex** или **sync.Cond**, которые были помещены в специальную очередь ожидания планировщика.

  1. Финальное состояние — паркинг (parking):
    Если на всех предыдущих этапах работу найти не удалось, текущий поток M **паркуется** (отсоединяется от своего **P** и уходит в сон, ожидая сигнала от ОС).
    *   Сам **P** (процессор, контекст планировщика) возвращается в пул свободных процессоров (`idle P`s).
    *   **M** (поток ОС) блокируется на condition variable или аналогичном примитиве, пока не появится новая работа (например, запустится новая горутина, проснется горутина из poller'а или другой поток украдет работу и "разбудит" спящий M).

Почему это важно для производительности

  • Минимизация простоя CPU: Алгоритм активно ищет работу, а не пассивно ждет. Это позволяет эффективно использовать все доступные ядра, особенно при высокой конкуренции или асинхронном I/O.
  • Балансировка нагрузки: Work stealing автоматически перераспределяет задачи с перегруженных потоков на менее загруженные.
  • Безопасность и масштабируемость: Большинство операций (управление локальной очередью, ворк-стилинг) выполняются без глобальных блокировок, что позволяет планировщику масштабироваться на десятки тысяч горутин и сотни потоков.

Итог: Пустая локальная очередь — не признак простоя системы, а триггер для активного перераспределения задач. Планировщик Go превращает потенциальный простой потока в возможность помочь другим потокам, обеспечивая высокий уровень утилизации CPU и низкие задержки в конкурентных сценариях.