Урок по треку Go 7 мин

Горутины и планировщик GMP

Горутина против потока, модель G, M, P, вытеснение, GOMAXPROCS, утечки горутин.

Горутина это функция, которая выполняется конкурентно с остальными. Запуск: go f(x). Горутины дёшевы: стартовый стек около 2 КБ, он растёт и сжимается по необходимости, поэтому в программе спокойно живут сотни тысяч горутин.

Горутина против потока ОС

Поток ОСГорутина
Стекфиксированный, обычно мегабайтыот 2 КБ, растёт
Переключениеядро ОСрантайм Go в пространстве пользователя
Созданиедорогоедешёвое

Горутины выполняются на небольшом числе потоков ОС. Этим занимается планировщик рантайма.

Модель GMP

  • G (goroutine): горутина со своим стеком и состоянием.
  • M (machine): поток ОС, который исполняет код.
  • P (processor): логический процессор, контекст для исполнения Go-кода. У каждого P своя локальная очередь готовых G (до 256). Число P задаёт GOMAXPROCS.

Чтобы выполнять Go-код, M должен держать P. Схема работы:

  1. Новая горутина попадает в локальную очередь текущего P (в слот runnext).
  2. M берёт G из локальной очереди своего P. Периодически (примерно раз в 61 тик) заглядывает в глобальную очередь, чтобы там ничего не голодало.
  3. Если локальная очередь пуста: глобальная очередь, сетевой поллер, а затем work stealing: кража половины очереди у другого P.
  4. Если G ушла в блокирующий системный вызов, M блокируется вместе с ней, а P передаётся другому M (handoff), чтобы остальные горутины не стояли.
  5. Сетевой ввод-вывод не блокирует поток: горутина паркуется, а netpoller (epoll, kqueue) будит её, когда сокет готов.

Вытеснение

До Go 1.14 переключение было кооперативным: в точках вызова функций. Цикл без вызовов мог занять P навсегда. С Go 1.14 есть асинхронное вытеснение: монитор sysmon замечает горутину, которая работает дольше ~10 мс, и прерывает её сигналом.

GOMAXPROCS

По умолчанию равен числу логических CPU. В контейнере с лимитом CPU это было проблемой: Go видел все ядра узла и создавал слишком много P, что давало троттлинг. Раньше ставили go.uber.org/automaxprocs, а с Go 1.25 рантайм сам учитывает лимит CPU cgroup на Linux и обновляет значение, если лимит меняется.

Завершение и синхронизация

main не ждёт горутины: когда main возвращается, программа завершается, даже если горутины не доделали работу. Ждать нужно явно: sync.WaitGroup, каналы, errgroup.

var wg sync.WaitGroup
for i := range 3 {
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println(i) // с Go 1.22 у каждой итерации свой i
    }()
}
wg.Wait()

Порядок вывода не определён. С Go 1.22 переменная цикла создаётся заново на каждой итерации, и классический баг «все горутины печатают 3» ушёл, если в go.mod указана версия 1.22 или новее.

runtime.Gosched() уступает процессор, runtime.NumGoroutine() показывает число горутин. У горутин нет ID в публичном API, и убить горутину снаружи нельзя: она должна завершиться сама (через context или закрытие канала).

Утечки горутин

Горутина, навсегда заблокированная на канале, никогда не освободит память. Типичные причины: никто не читает из канала, никто не закрывает канал, по которому идёт range, нет отмены по context. Искать через pprof профиль goroutine или go.uber.org/goleak в тестах.

Частые ошибки

  • Не ждать горутины и удивляться пустому выводу.
  • wg.Add внутри горутины: Wait может проскочить раньше.
  • Считать, что горутины выполнятся в порядке запуска.
  • Запускать горутину без способа её остановить.

Что спросят на собесе

  • Чем горутина отличается от потока.
  • Как устроен планировщик: G, M, P, локальные и глобальная очереди, work stealing.
  • Что происходит при блокирующем syscall и при сетевом вводе-выводе.
  • Кооперативное или вытесняющее планирование в Go.
  • Что такое GOMAXPROCS и что с ним в контейнерах.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.