При каких ситуациях горутина в Go блокируется и чего она ожидает?
Что проверяют
Знаете ли вы, что именно паркует горутину, и отличаете ли это от блокировки потока операционной системы. Вопрос почти всегда переходит в практику: как найти зависшую горутину в проде и что случится, если писать в закрытый канал.
Как отвечать
Сначала перечислите причины, потом объясните механику.
- каналы: приём из пустого канала, отправка в небуферизованный канал без готового получателя, отправка в заполненный буфер;
- nil-канал: и приём, и отправка встают навсегда. В
selectэтим пользуются намеренно: обнулили канал, и ветка перестала выбираться; - select: без
defaultждёт, пока хоть один case не станет готов, а пустойselect{}вешает горутину до конца жизни процесса; - примитивы sync:
Mutex.Lock,RWMutex,WaitGroup.Wait,Once.Do,Cond.Wait; - время и контекст:
time.Sleep,<-time.After(...),<-ctx.Done(); - ввод-вывод: сетевые операции уходят в netpoller, горутина паркуется, а поток забирает другую работу. Блокирующий системный вызов (файлы, cgo) занимает поток целиком, и
sysmonпримерно через 10 мс отцепляет P, заводя новый M.
Механика короткая: gopark снимает горутину с потока и переводит в ожидание, будит её тот, кто освободил ресурс. Если уснули все горутины сразу, рантайм падает с fatal error: all goroutines are asleep. Достаточно одного живого таймера или сетевого ожидания, чтобы этой ошибки не было, и вместо неё получится тихая утечка.
Закончите диагностикой: pprof по адресу goroutine?debug=2 показывает стеки с подписью вида [chan receive, 12 minutes], а блок- и мьютекс-профили включаются через runtime.SetBlockProfileRate и SetMutexProfileFraction.
Где спотыкаются
Говорят, что приём из закрытого канала блокирует. Он возвращает нулевое значение немедленно, а v, ok := <-ch отдаёт ok == false. Блокирует пустой открытый канал, а отправка в закрытый это паника.
Смешивают горутину и поток: «встала горутина, значит встал поток». На сетевом ожидании поток свободен и занят чужим кодом.
Забывают про утечку. Горутина пишет результат в небуферизованный канал, а вызывающий ушёл по таймауту, и она висит вечно. Лечится буфером на один элемент или веткой с ctx.Done().