Куда попадает функция при вызове go в Go? В какую очередь?
Что проверяют
Знаете ли вы модель GMP и что происходит в рантайме в момент go f(), а не только слово «горутина». Следом почти всегда спрашивают про work stealing и про то, что будет, если горутина надолго заняла процессор или ушла в блокирующий системный вызов.
Как отвечать
Начните с участников планировщика:
- G: сама горутина, структура с небольшим стеком, который стартует с пары килобайт и растёт по мере нужды;
- M: поток ОС, который исполняет код;
- P: логический процессор с локальной очередью. Их ровно
GOMAXPROCS, по умолчанию по числу ядер, а с Go 1.25 на Linux учитывается и лимит CPU контейнера.
При go f(x) аргументы вычисляются сразу, в вызывающей горутине. Затем runtime.newproc создаёт G и кладёт её в локальную очередь текущего P, точнее в слот runnext: новая горутина встаёт первой на запуск, а прежний обитатель runnext уходит в хвост локальной очереди. Локальная очередь это кольцевой буфер на 256 элементов. Если он полон, половина очереди вместе с новой G переносится в глобальную очередь, защищённую мьютексом.
Откуда P берёт работу: сначала runnext и локальная очередь, раз в 61 цикл планировщика он заглядывает в глобальную, чтобы та не голодала, затем проверяет сетевой поллер и, если пусто, крадёт половину очереди у другого P.
Добавьте про вытеснение: с Go 1.14 оно асинхронное, sysmon снимает горутину, которая работает дольше примерно 10 мс. При блокирующем системном вызове поток M отдаёт свой P другому потоку, и остальные горутины продолжают работать.
Где спотыкаются
Отвечают «в общую очередь». Глобальная очередь есть, но новая горутина попадает туда только при переполнении локальной.
Считают, что go f() запускает функцию немедленно и в порядке вызова. Гарантий порядка нет, синхронизация нужна явная: каналы, sync.WaitGroup или errgroup.
Путают G и поток ОС и делают выводы вроде «десять тысяч горутин это десять тысяч потоков». Потоков столько, сколько нужно под P и под заблокированные вызовы.