Для чего нужна локальная очередь у процессоров в Go?
Что проверяют
Знаете ли вы модель планировщика GMP и понимаете ли, зачем у каждого P своя очередь, если есть общая. Следом спрашивают про work stealing и про то, что происходит с P, когда горутина уходит в блокирующий системный вызов.
Как отвечать
Сначала три сущности:
- G: горутина со своим стеком и состоянием;
- M: поток ОС, который исполняет код;
- P: логический процессор, право исполнять Go-код. Их число задаёт
GOMAXPROCS, по умолчанию по числу ядер, а с Go 1.25 на Linux рантайм учитывает ещё и лимит CPU из cgroup.
У каждого P есть локальная очередь готовых к запуску горутин: кольцевой буфер на 256 элементов плюс отдельный слот runnext. Есть и глобальная очередь, защищённая мьютексом.
Зачем нужна локальная:
- без блокировок: владелец P берёт и кладёт горутины без общего мьютекса, поэтому планирование не упирается в одну точку на десятках ядер;
- локальность: горутина, созданная через
go, попадает вrunnextтекущего P и часто запускается следом, пока её данные ещё в кеше; - разгрузка: при переполнении половина локальной очереди переносится в глобальную.
Когда у P кончается работа, он ищет её по порядку: локальная очередь, глобальная, сетевой поллер, а затем крадёт половину очереди у случайного другого P. Чтобы горутины из глобальной очереди не голодали, планировщик раз в 61 цикл заглядывает туда в первую очередь.
При блокирующем системном вызове M уходит в ядро вместе с горутиной. Если вызов затягивается, sysmon отбирает P и передаёт его вместе с очередью другому M, и остальные горутины продолжают работать.
Где спотыкаются
Говорят, что локальная очередь есть у каждой горутины или что её пишут сами через каналы и срезы. Это внутренняя структура рантайма, из кода она не видна.
Путают P с ядром процессора. P это разрешение на исполнение, а потоков M может быть больше, чем P, особенно при системных вызовах.
Не знают про кражу работы и считают, что горутина навсегда привязана к P, на котором её создали.