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