Ответ
Планировщик Go (Go Scheduler) — это компонент рантайма Go, который отвечает за распределение горутин (goroutines) по рабочим потокам операционной системы (OS threads) для их выполнения.
Его главная задача — эффективно утилизировать процессорное время, управляя сотнями тысяч горутин на небольшом количестве системных потоков.
Ключевые компоненты (модель G-P-M):
- G (Goroutine): Легковесный поток, управляемый Go. Имеет свой стек, который может расти и сжиматься. Это то, что вы создаете с помощью ключевого слова
go
. - M (Machine): Поток операционной системы (OS thread). Это реальный поток, который выполняет код.
- P (Processor): Контекст для выполнения горутин. У каждого P есть локальная очередь горутин, готовых к выполнению. Количество P по умолчанию равно количеству ядер CPU (
runtime.GOMAXPROCS
).
Основные принципы работы:
Модель M:N: Планировщик отображает M горутин на N потоков ОС. Обычно N невелико (равно
GOMAXPROCS
), а M может быть огромным.Work-Stealing (Воровство работы): Когда у процессора P заканчиваются горутины в его локальной очереди, он может "украсть" половину горутин из очереди другого P. Это обеспечивает равномерную загрузку всех ядер.
Кооперативное вытеснение (Cooperative Preemption):
- Горутина добровольно уступает процессор при выполнении блокирующих операций (системные вызовы, работа с каналами, мьютексами).
- Начиная с Go 1.14, планировщик стал вытесняющим. Если горутина выполняется слишком долго без вызовов функций (например, в плотном цикле), рантайм может принудительно остановить её и поставить в очередь, дав поработать другим горутинам. Это решает проблему "зависания" планировшика на одной долгой задаче.
func main() {
// Запускаем две горутины
go func() {
fmt.Println("Goroutine 1")
}()
go func() {
fmt.Println("Goroutine 2")
}()
// runtime.Gosched() явно просит планировщик
// уступить процессор другим горутинам.
// Без этого вызова main может завершиться раньше,
// чем другие горутины успеют выполниться.
runtime.Gosched()
}