Планировщик Go: модель GMP
Сто тысяч горутин на двух ядрах едут и не спотыкаются. Спрашивают, за счёт чего: кто раскладывает горутины по потокам, что задаёт GOMAXPROCS и что случается, когда горутина уходит в блокирующий вызов.
Стержень: G - горутина, M - поток операционной системы, P - логический процессор со своей очередью; код исполняется только на связке M и P.
// Формулировки: «расскажи про GMP», «что задаёт GOMAXPROCS?», «что при блокирующем системном вызове?»
Три буквы, если запустить и посмотреть
Запустил в контейнере с двумя ядрами программу из двух строк: печатает runtime.NumCPU() и runtime.GOMAXPROCS(0). Обе выдали 2. Вот эта двойка и есть число P.
P - логический процессор: контекст с локальной очередью горутин и правом исполнять код. M - реальный поток операционной системы, тот самый, который раскладывает по ядрам уже ядро Linux. G - горутина: её стек, её состояние, её место в очереди. Горутина работает только когда собралась связка: поток взял логический процессор и крутит на нём горутину.
// Число P ограничивает параллелизм и больше ничего. Горутин при этом бывают сотни тысяч, а потоков - десятки: ни те, ни другие к GOMAXPROCS не привязаны.
- GMP
- горутина, поток операционной системы, логический процессор
- GOMAXPROCS
- число P, то есть потолок параллелизма
Очередь у каждого своя, а лентяй ворует
Картинка: у каждого логического процессора своя стопка карточек с задачами. Своя - потому что общую пришлось бы брать под блокировкой, а переключений в секунду там миллионы.
Кончились карточки у одного - он идёт к случайному соседу и забирает половину его стопки. В исходниках рантайма это буквально строка n = n - n/2 внутри runqsteal в proc.go. Нагрузка выравнивается сама, без начальника, который раздаёт задачи.
// Чтобы задачи из общей очереди не голодали, каждый 61-й тик планировщик заглядывает туда принудительно: в proc.go это условие pp.schedtick%61 == 0. Число выглядит случайным, но без него две горутины, бесконечно порождающие друг друга, заняли бы локальную очередь навсегда.
- work stealing
- простаивающий P забирает половину очереди соседа
- каждый 61-й тик
- принудительный заход в общую очередь ради честности
Блокирующий вызов и сетевое ожидание - разные вещи
Замерил на той же двухъядерной машине. Опыт первый: 30 горутин читают из пустого канала операционной системы (pipe без писателя) - настоящий блокирующий вызов. Потоков в процессе было 4, стало 33. Тридцать горутин утащили с собой тридцать потоков, хотя P всего два.
Опыт второй: 200 TCP-соединений, которые молчат, на каждом горутина в c.Read. Горутин 202, потоков создано 6 - ровно столько же, сколько до соединений. Сеть в Go идёт через netpoller поверх epoll: горутину снимают с исполнения, поток уходит работать дальше, а когда сокет готов, опросчик кладёт горутину обратно в очередь.
// Отсюда и «Go держит десятки тысяч соединений»: держит их опросчик, а не потоки. Файловый ввод-вывод и вызовы в C через опросчик не идут, там потоки тратятся по-настоящему - как в моём первом опыте.
- netpoller
- опросчик готовности сокетов, потока не занимает
- блокирующий системный вызов
- держит поток, но освобождает P соседям
Как отвечать: «Как устроен планировщик Go и что задаёт GOMAXPROCS?»
Модель называется GMP. G - горутина со своим стеком и состоянием, M - реальный поток операционной системы, P - логический процессор, то есть контекст с локальной очередью горутин и правом исполнять код. Работает горутина только на связке потока с логическим процессором. GOMAXPROCS задаёт число P, по умолчанию равное числу доступных ядер, и ограничивает оно параллелизм - не количество горутин и не количество потоков. Разница заметна на замере: я держал 30 горутин в блокирующих чтениях при GOMAXPROCS равном двум, и рантайм завёл 33 потока. Очереди у каждого P свои, потому что общая потребовала бы блокировки на каждом переключении; когда очередь пустеет, логический процессор крадёт половину задач у случайного соседа - так нагрузка выравнивается без центрального распределителя. Отдельно про ожидание: сетевое идёт через netpoller поверх epoll и потоков не занимает вовсе. У меня 200 молчащих соединений жили на том же числе потоков, что и пустая программа. И с версии 1.14 планировщик вытесняет горутины асинхронно, по сигналу, поэтому счётный цикл без вызовов функций больше не подвешивает поток и не мешает сборщику.
Сильный ответ: модель названа с ролью каждой буквы, GOMAXPROCS привязан к параллелизму, а не к горутинам, кража работы объяснена через локальность очередей, разведены блокирующий вызов и сетевое ожидание, добавлено асинхронное вытеснение. Числа из собственного замера снимают у собеседующего половину уточняющих вопросов.
На чём валят
- −Считают, что GOMAXPROCS ограничивает число горутин. Он ограничивает параллелизм.
- −Уверены, что потоков в процессе ровно GOMAXPROCS: у меня 30 блокирующих чтений подняли счётчик потоков с 4 до 33.
- −Валят в одну кучу блокирующий системный вызов и сетевое ожидание. Второе потоков не занимает.
- −Ищут центрального распределителя задач. Очереди локальные, пустой P сам крадёт половину у соседа.
- −Пересказывают старую байку про счётный цикл, вешающий поток: с версии 1.14 горутину вытесняют сигналом.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 7, остальные разбираются в тренажёре.
- На какой модели построен планировщик горутин в Go?A)G-M-P: горутины (G) на потоки ОС (M) через процессоры (P)B)Один поток ОС на каждую горутину, строго один к одномуC)Кооперативная модель без потоков ОС — всё в одном системном потокеD)Отдельный процесс ОС на каждую горутину с общей памятью
показать ответ и разбор
+A)G-M-P: горутины (G) на потоки ОС (M) через процессоры (P)// разбор: Планировщик Go — модель G-M-P: G (горутина), M (machine, поток ОС), P (processor, логический процессор с локальной очередью горутин). M исполняет горутины из очереди своего P; число P задаёт GOMAXPROCS. Так рантайм мультиплексирует тысячи G на несколько M (M:N), переключая их в user space, без ухода в ядро.
- Что задаёт GOMAXPROCS?A)Максимальное число горутин, которые можно создать одновременноB)Размер стека каждой горутины в килобайтахC)Сколько потоков ОС могут одновременно исполнять Go-кодD)Число ядер, которые ОС выделит процессу под нагрузкой
показать ответ и разбор
+C)Сколько потоков ОС могут одновременно исполнять Go-код// разбор: GOMAXPROCS — число P, то есть сколько потоков ОС могут ОДНОВРЕМЕННО выполнять Go-код (степень параллелизма). По умолчанию равен числу доступных ядер. Он не ограничивает число горутин (их могут быть сотни тысяч поверх) и не задаёт размер стека. Горутина на блокирующем syscall не держит P — рантайм отдаёт его другим.
- Горутина ушла в блокирующий системный вызов. Что планировщик делает с её P?A)Блокирует весь P до возврата из системного вызова вместе с горутинойB)Отдаёт P другому потоку, чтобы тот исполнял остальные горутиныC)Завершает горутину по таймауту, освобождая потокD)Переносит остальные горутины этого P на процессы ОС
показать ответ и разбор
+B)Отдаёт P другому потоку, чтобы тот исполнял остальные горутины// разбор: Когда M застревает на блокирующем syscall, рантайм отвязывает его P и отдаёт другому (при нужде — новому) M, чтобы очередь горутин этого P исполнялась дальше и ядро не простаивало. Вернувшийся из syscall M попробует снова взять P или припаркуется. Плюс work stealing: P с пустой очередью крадёт горутины у других — так балансируется нагрузка.
- Что делает планировщик, когда локальная очередь одного P опустела?A)Ждёт, пока другие P сами передадут ему часть своих горутинB)Переносит все горутины в общую глобальную очередь и берёт оттудаC)Останавливает поток и возвращает его операционной системеD)Забирает часть горутин из очереди другого P — это work stealing
показать ответ и разбор
+D)Забирает часть горутин из очереди другого P — это work stealing// разбор: У каждого P своя локальная очередь — это дешевле общей с блокировкой. Опустела — P идёт красть примерно половину задач у случайного соседа, а заодно поглядывает в глобальную очередь и в сетевой опросчик. Так нагрузка выравнивается сама, без центрального распределителя: простаивающий процессор находит работу, а не ждёт раздачи.
- Горутина крутится в цикле без вызовов функций и обращений к каналам. Заблокирует ли она свой поток навсегда?A)Да: без точки переключения планировщик отобрать управление не можетB)Да, если GOMAXPROCS равен единице, иначе её вытеснит соседний PC)Нет: с версии 1.14 планировщик вытесняет горутины асинхронно, по сигналуD)Нет: рантайм разбивает длинные циклы на части при компиляции
показать ответ и разбор
+C)Нет: с версии 1.14 планировщик вытесняет горутины асинхронно, по сигналу// разбор: До 1.14 планирование было кооперативным: переключение случалось лишь в точках вроде вызова функции или работы с каналом, и плотный счётный цикл действительно вешал поток — от него страдал и сборщик мусора, которому нужна остановка мира. Теперь рантайм посылает потоку сигнал и вытесняет горутину принудительно, так что зависание из-за такого цикла ушло в историю.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.