Как работает Sheduler?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает Go Scheduler
Go scheduler — это часть рантайма Go, реализующая модель параллелизма M:N, где множество горутин (G) планируется на множестве нитей ОС (M), которые выполняются на логических процессорах (P). Это ключевой компонент, обеспечивающий эффективное и масштабируемое выполнение конкурентных программ на многопроцессорных системах.
Основные сущности
Работу scheduler'а невозможно понять без знакомства с его фундаментальными компонентами:
- G (Goroutine) — легковесная нить, абстракция пользовательского конкурентного кода. Горутина содержит стек, указатель на выполняемую функцию, состояние (
_Gidle,_Grunnable,_Grunning,_Gwaiting,_Gdead) и другие метаданные для планирования. - M (Machine) — абстракция нити ядра ОС (например, POSIX-нити). Именно
Mвыполняет код горутины на реальном процессоре. КоличествоMможет быть значительно больше количества ядер CPU. - P (Processor) — логический процессор, ресурс, необходимый
Mдля выполнения кодаG.Pуправляет локальной очередью исполняемых горутин (runq). КоличествоPпо умолчанию равно количеству логических ядер CPU (GOMAXPROCS) и определяет реальный параллелизм программы.
// Упрощенное представление структур в runtime (для понимания концепции)
type g struct {
stack stack
sched gobuf
goid int64
status uint32 // _Gidle, _Grunnable и т.д.
m *m // текущий m, если running
// ... другие поля
}
type p struct {
id int32
status uint32 // _Pidle, _Prunning
m muintptr // привязанный m
runqhead uint32
runqtail uint32
runq [256]guintptr // локальная очередь из 256 горутин
runnext guintptr // специальная "следующая" горутина
// ... другие поля
}
type m struct {
g0 *g // специальная горутина для планирования
curg *g // текущая исполняемая горутина
p puintptr // привязанный p
nextp puintptr
// ... другие поля
}
Принцип работы планировщика
-
Инициализация: При запуске программы runtime создает количество
P, равноеGOMAXPROCS. КаждыйPимеет свою локальную очередь (runq) из 256 горутин. Также создается одна "спящая" очередь (global runq) вschedструктуре. -
Создание горутины: При вызове
go func()создается новая горутинаG. Она помещается в локальную очередь (runq) тогоP, с которым ассоциированM, выполняющий текущий код. Это обеспечивает локальность данных и снижает конкуренцию за блокировки. -
Планирование выполнения (
schedule):
* **Поток `M`** ищет работу: он должен быть привязан к свободному `P`, чтобы начать выполнение горутин.
* Алгоритм выбора следующей горутины для выполнения следует приоритетам:
* Проверить `runnext` у текущего `P` (специальная высокоприоритетная горутина, например, только что разблокированная).
* Взять горутину из локальной очереди `runq` текущего `P` (работает по принципу **кольцевого буфера**).
* Попытаться **украсть** горутину из локальной очереди другого `P` (work-stealing). Это критически важный механизм для балансировки нагрузки и предотвращения простаивания ядер.
* Взять горутину из глобальной очереди (`sched.runq`).
* Проверить **сетевой поллинг** (`netpoller`) — горутины, ожидающие сетевых событий.
* Если работы нет, `M` может отсоединиться от `P` и заснуть.
- Прерывания (преэмпция):
Scheduler должен предотвращать монополизацию CPU одной горутиной. Это реализуется через:
* **Кооперативную многозадачность** на точек входа функций (вызовов функций, циклов `for` в некоторых случаях). Это устаревший, но все еще присутствующий механизм.
* **Вытесняющую многозадачность (preemption)** на основе **сигналов ОС**. Специальная системная нить (`sysmon`) помечает долго выполняющиеся горутины (`G`), и отправляет асинхронный сигнал (например, `SIGURG` на Linux) соответствующему `M`. Обработчик сигнала проверяет флаг и дает возможность scheduler'у переключиться на другую горутину. Это гарантирует, что даже плотный цикл `for` без вызовов функций может быть прерван.
Ключевые механизмы
- Work-Stealing: Когда
Pопустошает свою локальную очередь, он сначала пытается взять работу из глобальной очереди, а затем случайным образом выбирает другогоPи "ворует" половину горутин из его локальной очереди. Это делает планировщик децентрализованным и масштабируемым. - Sysmon (System Monitor): Фоновая нить, не привязанная к
P. Она выполняет критически важные фоновые задачи:
* Сетевой поллинг (просыпает горутины, ожидающие I/O).
* Принудительная сборка мусора.
* Вытеснение долго выполняющихся горутин (preemption).
* Возвращение `P` в пул, если его `M` заблокировался в системном вызове на долгое время, и создание/привязка нового `M` к этому `P` для поддержания параллелизма.
- Блокировки: Когда горутина блокируется (канал, мьютекс, системный вызов), текущий
Mможет отсоединиться от горутины иP, чтобы позволитьPвыполнять другие горутины на другомM. Когда блокировка снимается, горутина пытается найти свободныйPдля продолжения работы.
Пример иллюстрации работы
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
for i := 0; i <黏合; i++ {
fmt.Printf("Worker %d: %d\n", id, i)
runtime.Gosched() // Явная точка кооперативного перепланирования
}
}
func main() {
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // Число P
for i := 0; i < 5; i++ {
go worker(i) // Создаются 5 G, попадают в локальные очереди P
}
time.Sleep(1 * time.Second)
}
В этом примере 5 горутин конкурируют за время на P процессорах. Вызов runtime.Gosched() добровольно уступает текущий P, помещая горутину обратно в очередь и запуская следующую.
Выводы
Go scheduler — это гибридная, вытесняющая, work-stealing система планирования, оптимизированная для низких задержек и высокой пропускной способности в многопоточных окружениях. Его главные преимущества:
- Масштабируемость благодаря децентрализованным очередям и work-stealing.
- Эффективность благодаря локальным очередям, минимизирующим блокировки.
- Справедливость благодаря вытеснению и работе sysmon.
- Интеграция с сетевым I/O через netpoller, что делает блокирующие операции неблокирующими для потоков ОС.
Понимание этих принципов позволяет писать более эффективные конкурентные программы, правильно настраивать GOMAXPROCS и избегать паттернов, которые могут деградировать производительность scheduler'а (например, создание миллионов "спящих" горутин, длительные вычисления без точек вызова функций).