Зачем запускать много горутин на процессоре с ограниченным числом ядер
Что проверяют
Различаете ли вы конкурентность и параллелизм и понимаете ли, как планировщик Go раскладывает горутины по потокам. Следом обычно спрашивают, ускорит ли миллион горутин чисто вычислительную задачу и чем грозит неограниченный запуск.
Как отвечать
Главный аргумент: большая часть реальных задач не считает, а ждёт сеть, базу, диск или соседний сервис. Горутина в ожидании не занимает ядро, поэтому тысячи горутин на четырёх ядрах держат тысячи одновременных запросов.
Механика, которую стоит назвать:
- модель G-M-P: горутины (G) выполняются на потоках ОС (M), которым нужен логический процессор (P). Число P задаёт
GOMAXPROCS, по умолчанию это число ядер, а с Go 1.25 на Linux учитывается и CPU-лимит cgroup в контейнере; - netpoller: сетевое ожидание паркует горутину через epoll или kqueue, и поток сразу берёт следующую. Блокирующий системный вызов, например чтение файла, занимает поток, и рантайм отдаёт его P другому потоку;
- дешевизна: стартовый стек горутины 2 КБ и растёт по мере нужды, у потока ОС стек измеряется мегабайтами. Переключение идёт в пользовательском пространстве, без ядра ОС;
- вытеснение: с Go 1.14 рантайм асинхронно прерывает горутину в тесном цикле, так что жадная горутина не держит свой P бесконечно.
Для CPU-bound задач ответ другой: горутин больше, чем GOMAXPROCS, расчёт не ускорят, только добавят переключений. Там нужен пул воркеров размером с число P, например errgroup с SetLimit.
Где спотыкаются
Отвечают «больше горутин, значит быстрее», не разделяя I/O и вычисления. Для вычислений прирост заканчивается на числе ядер.
Запускают горутину на каждый элемент без ограничения. Памяти горутинам хватит, а вот пул соединений к базе или внешний API под таким потоком запросов ляжет, поэтому нужны семафор или пул.
Забывают про утечки: горутина, которая навсегда ждёт канал без context и отмены, живёт до конца процесса. Их ищут через pprof по профилю goroutine и через goleak в тестах.