Skip to content

Планировщик в Go: кооперативная многозадачность

Планировщик ОС является упреждающим планировщиком. Это означает, что ты не можешь предсказать, что планировщик собирается делать в любой момент времени. Ядро принимает решения, и все не детерминировано. Приложения, работающие поверх ОС, не контролируют то, что происходит внутри ядра с помощью планирования, если только они не используют примитивы синхронизации, такие как атомарные инструкции и вызовы мьютексов.

Планировщик Go является частью среды выполнения Go, а среда выполнения Go встроена в приложение (userspace). Это означает, что планировщик Go работает в пользовательском пространстве над ядром. Текущая реализация планировщика Go — это не вытесняющий, а кооперативный планировщик. Быть кооперативным планировщиком означает, что планировщику нужны четко определенные события пользовательского пространства, которые происходят в безопасных точках кода (safe points), чтобы принимать решения об остановке горутины и переключиться на другие горутины.

Что замечательно в кооперативным планировщике Go, так это то, что он выглядит и ощущается упреждающим. Ты не можешь предсказать, что собирается делать планировщик Go. Это связано с тем, что принятие решений для этого кооперативного планировщика находится не в руках разработчиков, а в среде выполнения Go (in runtime). Важно думать о планировщике Go как об упреждающем планировщике, и, поскольку планировщик не детерминирован, это не так уж сложно. То есть, мы должны писать код не задумываясь о конкретном времени исполнения асинхронной части кода в наших приложениях.

Сейчас форсированное переключение горутин осуществляется по заданной константе forcePreemptNS — константа равная 10мс. Такое время отводится на отработку горутины, но при условии, что она находится в safe point, данные безопасные точки проставляются на этапе компиляции.

Состояния горутин

Как и потоки, горутины имеют те же три состояния высокого уровня. Они определяют роль, которую планировщик Go играет с любой данной горутиной. Горутина может находиться в одном из трех состояний: Waiting, Runnable или Executing.

Waiting: означает, что горутина остановлена и ожидает чего-то, чтобы продолжить работу. Это может быть по таким причинам, как ожидание операционной системы (системные вызовы) или вызовы синхронизации (атомарные операции и операции с мьютексом). Эти типы задержек являются основной причиной низкой производительности приложений на Golang.

Runnable: означает, что горутине нужно время на M, чтобы она могла выполнять назначенные инструкции. Если у тебя много горутин, которым нужно время на исполнение, то горутины должны ждать дольше, чтобы получить время. Кроме того, индивидуальное количество времени, которое получает каждая горутина, сокращается по мере того, как все больше горутин соревнуются за время. Этот тип задержки планирования также может быть причиной низкой производительности.

Executing: это означает, что горутина была помещена на M и выполняет свои инструкции. Работа, связанная с приложением, ведется.