Жизненный цикл goroutine и модель G-M-P
Goroutine — лёгкая единица выполнения, управляемая runtime Go. Её стек растёт и сжимается по необходимости, а runtime мультиплексирует много goroutine на меньшее число потоков ОС. Однако goroutine не «бесплатна»: каждая требует памяти, записи в scheduler и ясного способа завершиться.
Зачем это на интервью
Тема проверяет, понимаете ли вы, почему go f() не даёт ни ожидания, ни отмены, что именно ограничивает GOMAXPROCS, и как отличать проблему Go scheduler от блокировки на I/O или lock.
Минимум для E4
- Объяснять, что goroutine запускается асинхронно и программа завершается вместе с
main. - Знать роли G (goroutine), M (OS thread), P (ресурс выполнения) и
GOMAXPROCS. - Передавать
context.Contextв долгую работу и ждать workers черезsync.WaitGroupили результат. - Понимать, что блокирующий syscall может потребовать дополнительный M, а P не равен числу goroutine.
package main
import (
"context"
"fmt"
"sync"
)
func worker(ctx context.Context, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
fmt.Println(job * job)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go worker(ctx, jobs, &wg)
jobs <- 4
close(jobs) // единственный producer сообщает конец входа
wg.Wait()
}Углубление для E5/Senior
P содержит локальную очередь runnable G и нужен M для выполнения Go-кода; число P ограничено GOMAXPROCS (по умолчанию runtime выбирает доступный CPU). Scheduler использует локальные очереди, глобальную очередь и work stealing. Когда M блокируется в syscall, runtime может отвязать P и дать его другому M, чтобы не останавливать Go-код. При CPU-bound нагрузке больше runnable G, чем P, повышают конкуренцию и latency, но не создают дополнительный CPU.
Асинхронная preemption позволяет runtime остановить долго работающую goroutine в безопасной точке, поэтому бесконечный CPU-цикл не должен рассчитывать на добровольный yield. Это механизм справедливости, а не контракт latency. Диагностируют scheduler через go tool trace, profile goroutine и метрики runtime; изменение GOMAXPROCS — измеряемый deployment-параметр, особенно при CPU quota контейнера.
Ключевые понятия
- G — дескриптор goroutine: стек, состояние, функция и метаданные.
- M — поток ОС, исполняющий Go-код или системный вызов.
- P — логический ресурс runtime, необходимый M для запуска Go-кода; не процессор ОС.
- Runnable/blocked — runnable ждёт P/M/CPU, blocked ждёт канал, lock, I/O, таймер или другое событие.
- Goroutine leak — goroutine, которая больше не полезна, но навсегда ждёт и удерживает ресурсы.
Типовые вопросы
- Что делает
go f()?- Создаёт новую goroutine и планирует её выполнение; порядок с текущей goroutine не определён, а вызвавшая сторона не ждёт результат.
- Что означает G-M-P?
- G — работа, M — OS thread, P — разрешение исполнять Go-код. M выполняет G только имея P.
GOMAXPROCS— это число OS threads?- Нет. Это максимум P, то есть параллельного Go-кода; runtime может создать больше M из-за syscalls и других нужд.
- Почему нельзя просто поднять число goroutine для CPU-задачи?
- После насыщения P/CPU растут очередь, переключения и хвост задержки. Нужен ограниченный пул и измерение.
- Как гарантировать завершение goroutine?
- Дать ей наблюдаемый вход завершения: закрытие входного канала,
ctx.Done(), ограниченный результат иWaitGroupдля ожидания.
- Дать ей наблюдаемый вход завершения: закрытие входного канала,
- Как искать утечку?
- Сравнить goroutine profile/метрику до и после нагрузки, найти stack ожидания, затем исправить владельца cancellation или закрытия.
Практика
- Напишите worker, который ждёт канал или отмену. Готово:
wg.Wait()возвращается и при закрытии jobs, и приcancel(). - Создайте утечку: goroutine отправляет в канал без получателя; найдите её goroutine profile. Готово: стек показывает блокировку send, после исправления число goroutine возвращается к baseline.
- Проверьте CPU-bound workload при разных
GOMAXPROCS. Готово: записаны throughput, p99 и CPU quota; выбранное значение обосновано метриками.
Частые ошибки и ловушки
- Считать завершение
mainожиданием всех goroutine. - Передавать работу в goroutine, игнорируя
ctx.Done(). - Путать P с физическим ядром и M с числом P.
- Вызывать
runtime.Gosched()как универсальное исправление дизайна. - Менять
GOMAXPROCSбез учёта cgroup quota и профиля нагрузки.
Связанные темы
Конкурентность · sync-примитивы · Pipeline и отмена · Планирование CPU