Почему может происходить дедлок при записи в канал в Go
Что проверяют
Понимаете ли вы условия блокировки отправки и знаете ли, что рантайм ловит не все случаи.
Как отвечать
Отправка в канал блокируется в двух ситуациях: небуферизованный канал без готового получателя и буферизованный канал с заполненным буфером. Взаимная блокировка возникает, когда состояние никогда не изменится.
Типичные случаи:
1. Отправка и получение в одной горутине.
ch := make(chan int)
ch <- 1 // блокируемся навсегда: читать некому
fmt.Println(<-ch)Строка чтения не выполнится, потому что выполнение остановилось на предыдущей.
2. Читатель ушёл раньше времени. Получатель прервал цикл или вернулся по ошибке, а отправитель продолжает писать в канал, который никто не читает.
3. Буфер переполнен, потребитель не справляется или упал.
4. Забытое закрытие. Получатель ждёт закрытия в цикле перебора, отправитель ждёт места. Обе стороны стоят.
5. Отправка после отмены. Работа отменена по контексту, потребитель вышел, а горутина всё ещё пытается отдать результат.
Важное про обнаружение: рантайм печатает сообщение о том, что все горутины спят, только если заблокированы абсолютно все. В работающем сервисе всегда есть живые обработчики, поэтому такие зависшие горутины накапливаются молча. Видно это по росту их числа в профиле, а не по ошибке.
Как не допускать:
- отправка в конструкции выбора вместе с сигналом отмены, чтобы у отправителя был выход;
- вариант с ветвью по умолчанию, если значение можно потерять;
- чёткая ответственность за закрытие: закрывает отправитель, и только один;
- при нескольких отправителях канал закрывает отдельная горутина после ожидания группы;
- читать результаты параллельно с работой, а не после завершения всех отправителей.
Где спотыкаются
Пишут и читают небуферизованный канал в одной горутине.
Не предусматривают выход отправителя при отмене.
Считают, что рантайм всегда сообщит о взаимной блокировке.
Закрывают канал со стороны получателя.