Ответ
Планировщик Go отвечает за распределение горутин по потокам операционной системы для выполнения. Он использует эффективную модель, известную как G-M-P.
Компоненты модели:
- G (Goroutine): Легковесный поток, управляемый рантаймом Go. Имеет свой небольшой стек, который может расти по мере необходимости. Это то, что мы создаем с помощью ключевого слова
go
. - M (Machine): Поток операционной системы (kernel thread). Это реальный поток, который выполняет код. В программе может быть много M, но не все они могут быть активны одновременно.
- P (Processor): Контекст для выполнения. Он связывает горутины (G) с потоками ОС (M). Количество P по умолчанию равно количеству ядер CPU (
GOMAXPROCS
). У каждого P есть своя локальная очередь исполняемых горутин (LRQ - Local Run Queue).
Принцип работы (Work-Stealing):
- Для выполнения работы поток M должен получить в распоряжение контекст P.
- M берет горутину G из локальной очереди своего P и выполняет ее.
- Если локальная очередь P пуста, P пытается "украсть" (work-stealing) половину горутин из очереди другого P или из глобальной очереди. Это обеспечивает равномерную загрузку всех потоков ОС.
- Если горутина блокируется на системном вызове (например, чтение файла), M отсоединяется от P вместе с этой G, а рантайм может прикрепить к этому P другой M, чтобы продолжить выполнение других горутин.
Пример с sync.WaitGroup
:
Для ожидания завершения горутин идиоматично использовать sync.WaitGroup
, а не time.Sleep
.
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // Увеличиваем счетчик WaitGroup
go func(n int) {
defer wg.Done() // Уменьшаем счетчик по завершении горутины
fmt.Printf("Горутина %d выполняетсяn", n)
}(i)
}
wg.Wait() // Ожидаем, пока счетчик не станет равен нулю
fmt.Println("Все горутины завершили работу.")
}
С Go 1.14 планировщик является вытесняющим, что не позволяет одной горутине монополизировать поток выполнения надолго.