Ответ
Планировщик Go реализует модель M:N, где M горутин выполняются на N потоках операционной системы. Это позволяет эффективно управлять тысячами горутин на небольшом количестве системных потоков. В основе планировщика лежит концепция G-P-M.
-
G (Goroutine): Легковесный поток, управляемый рантаймом Go. Имеет свой собственный стек, который может расти и сжиматься. Это то, что вы создаете с помощью ключевого слова
go. -
M (Machine): Системный поток (OS Thread), управляемый операционной системой. Это реальный исполнитель кода.
-
P (Processor): Контекст для выполнения. Это ключевая абстракция, которая связывает горутины с потоками. У каждого
Pесть локальная очередь исполняемых горутин. КоличествоPпо умолчанию равно количеству ядер CPU и контролируется переменнойruntime.GOMAXPROCS.
Как это работает:
- Для выполнения
Pдолжен быть привязан кM. PберетGиз своей локальной очереди и выполняет ее наM.- Work Stealing (Воровство работы): Если у одного
Pзаканчиваются горутины в очереди, он может "украсть" половину горутин из очереди другогоP, чтобы сбалансировать нагрузку и не простаивать. - Блокирующие системные вызовы: Если
GнаMвыполняет блокирующий системный вызов (например, чтение файла),Pотсоединяется от этогоM(вместе с заблокированнойG), находит другой свободныйM(или создает новый) и продолжает выполнять другие горутины из своей очереди. Это предотвращает блокировку целого потока ОС.
func main() {
// Устанавливаем количество "процессоров" (P), которые будут
// одновременно выполнять горутины.
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
fmt.Printf("Горутина %d выполняетсяn", i)
// какая-то работа
time.Sleep(10 * time.Millisecond)
}(i)
}
wg.Wait()
}
Эта модель позволяет Go достигать высокой степени параллелизма с минимальными накладными расходами на переключение контекста по сравнению с традиционными потоками ОС.
Ответ 18+ 🔞
Давай разберёмся, как эта штука работает, а то звучит как какая-то мартышлюшка с аббревиатурами. G-P-M, блядь. Не ГПУ, а вот эта хуйня.
Представь себе такую картину: у тебя есть горутины (G) — это как легковесные тараканы, которых ты плодишь пачками командой go. Их дохуя, они шустрые, у каждого свой маленький стек, который может раздуваться, если надо. Тысячи их, блядь.
Но выполнять-то их надо на чём-то реальном, да? Вот тут появляются потоки ОС (M) — это уже серьёзные дядьки, системные, тяжёлые. Каждый такой поток — это Machine, железный исполнитель.
А теперь, внимание, самый важный пиздец — процессор (P). Это не физическое ядро, а такая абстрактная хуйня, контекст. Его главная задача — быть сводником между тараканами-горутинами и потоками-тяжеловесами. У каждого P есть своя локальная очередь тараканов, готовых к работе. Количество этих P по умолчанию равно твоим ядрам процессора. Хочешь больше — дергай runtime.GOMAXPROCS, но обычно это как вставить палку в колёса, если не знаешь, зачем.
Как вся эта банда работает:
- Чтобы что-то делать, P должен прилепиться к какому-нибудь свободному M. Связка готова.
- P хватает первого таракана G из своей очереди и суёт его на поток M — тот начинает его выполнять.
- Воровство работы (Work Stealing) — вот где начинается цирк! Если один P простаивает, потому что его очередь пуста, он не будет сидеть и чесать яйца. Он подкрадётся к другому, более занятому P, и украдёт половину его тараканов из очереди! Честное воровство, сбалансированное, блядь. Красота.
- А если горутина тупит? Ну, например, читает файл или ждёт сетевой ответ — это блокирующий вызов. Если бы поток M встал колом и ждал, это был бы пиздец и неэффективность. Поэтому система делает гениальную вещь: P отсоединяется от этого M вместе с заблокированной горутиной, находит другой свободный M (или создаёт новый) и продолжает гонять других тараканов из своей очереди! Заблокированная горутина потом, когда проснётся, встанет в очередь какого-нибудь P и будет ждать своего часа. Ни один поток ОС не простаивает просто так! Ёперный театр!
func main() {
// Говорим: "Давай, используй два процессора (P) для всей этой движухи".
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
fmt.Printf("Горутина %d выполняетсяn", i)
// какая-то работа
time.Sleep(10 * time.Millisecond)
}(i)
}
wg.Wait()
}
Вот за счёт всей этой хитрожопой архитектуры Go и может гонять сотни тысяч горутин, не падая в обморок от переключения контекста, как это делают классические потоки. Всё дело в том, что переключает он не потоки ОС (это дорого), а этих легковесных тараканов G внутри потоков M. И делает это умно, с воровством и отцеплением. Гениально и немного по-бандитски, если вдуматься.