Ответ
Планировщик Go реализует модель M:N, где M горутин выполняются на N потоках операционной системы. Количество потоков ОС, которые могут одновременно выполнять код Go, по умолчанию равно количеству доступных ядер процессора и регулируется переменной runtime.GOMAXPROCS
.
Рассмотрим пример с 4 горутинами и 4 ядрами:
Настройка: При запуске Go-приложения
GOMAXPROCS
будет установлено в 4. Это означает, что рантайм создаст 4 системных потока (P в модели GMP), каждый из которых может быть привязан к своему ядру процессора.Идеальный сценарий (CPU-bound задачи): Если все 4 горутины выполняют исключительно вычислительные операции (не блокируются), планировщик, скорее всего, распределит каждую горутину на отдельный поток/ядро, и они будут выполняться параллельно.
Реальный сценарий (с блокировками): Если одна из горутин блокируется (например, ждет ответа по сети, чтения файла или данных из канала), планировщик немедленно "усыпляет" ее, отвязывает от потока и ставит в очередь на выполнение другую, готовую к работе горутину. Это позволяет максимально утилизировать процессорное время.
import (
"fmt"
"runtime"
"sync"
)
func main() {
// Устанавливаем GOMAXPROCS равным количеству ядер (в современных версиях Go это делается автоматически)
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Printf("Running with %d logical CPUsn", runtime.NumCPU())
var wg sync.WaitGroup
wg.Add(4)
for i := 0; i < 4; i++ {
go func(id int) {
defer wg.Done()
// Какая-то работа
fmt.Printf("Goroutine %d is workingn", id)
}(i)
}
wg.Wait()
}
Вывод:
- Распределение не является фиксированным. Планировщик динамически переключает горутины между потоками для достижения максимальной пропускной способности.
- Для задач, требующих привязки к конкретному потоку ОС (например, для интеграции с некоторыми C-библиотеками), можно использовать
runtime.LockOSThread()
.