> Как определить дедлок в Go и когда он возникает? (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Т-Банк

Стек: Go

> Пример ответа

Короткий ответ

Дедлок в Go - это состояние, когда горутины навсегда блокируют друг друга, ожидая ресурсы, которые никогда не будут освобождены. Он возникает при взаимном ожидании каналов, мьютексов или других примитивов синхронизации. Go runtime обнаруживает дедлок только в случае, когда все горутины заблокированы, и завершает программу с ошибкой fatal error: all goroutines are asleep - deadlock!. Частичные дедлоки (когда часть горутин работает) не детектируются автоматически.

Подробное объяснение

Дедлок - это классическая проблема конкурентного программирования, и Go не исключение. Основные причины возникновения:

  1. Взаимное ожидание каналов: горутина A отправляет в канал, который читает горутина B, а B отправляет в канал, который читает A. Обе блокируются навсегда.

  2. Взаимная блокировка мьютексов: горутина A захватывает mutex1 и ждёт mutex2, а горутина B захватывает mutex2 и ждёт mutex1.

  3. Ожидание на unbuffered канале без получателя: отправка в unbuffered канал блокируется, пока не появится получатель. Если получатель никогда не появится - дедлок.

  4. Ожидание на buffered канале с заполненным буфером: отправка в полный buffered канал блокируется аналогично.

  5. Блокировка на sync.WaitGroup: если Wait() вызывается до того, как все Done() будут выполнены, и горутины не могут продолжиться.

Go runtime использует детектор дедлоков, который срабатывает, когда все горутины находятся в состоянии блокировки. В этом случае runtime выводит стек-трейсы всех горутин и завершает программу. Однако если хотя бы одна горутина активна, дедлок не будет обнаружен - программа просто зависнет.

Важно понимать разницу между дедлоком и livelock (когда горутины активны, но не прогрессируют) и starvation (когда одна горутина голодает из-за несправедливого планирования).

На практике

На практике дедлоки чаще всего возникают из-за невнимательности при работе с каналами и мьютексами. Основные паттерны, которые приводят к дедлокам:

  • Использование unbuffered каналов без гарантии наличия получателя.
  • Захват нескольких мьютексов в разном порядке в разных горутинах.
  • Рекурсивный вызов функций, которые захватывают один и тот же мьютекс (в Go мьютексы не реентерабельны).
  • Ожидание результата от горутины, которая сама ждёт результат от текущей горутины.

Для предотвращения дедлоков применяются:

  • Таймауты при работе с каналами (через select с time.After).
  • Единый порядок захвата мьютексов (например, по адресу объекта).
  • Использование sync.Mutex вместо нескольких отдельных.
  • Контекст с отменой (context.Context) для управления жизненным циклом горутин.
  • Инструменты статического анализа, например go vet с флагом -copylocks.

Пример кода

GO
// Пример дедлока на unbuffered канале
func deadlockExample() {
ch := make(chan int)
ch <- 42 // блокируется навсегда, нет получателя
fmt.Println(<-ch)
}
// Пример взаимного ожидания каналов
func mutualChannelDeadlock() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
<-ch1
ch2 <- 1
}()
<-ch2 // блокируется, ждёт данные из ch2
ch1 <- 1 // никогда не выполнится
}
// Пример дедлока на мьютексах
func mutexDeadlock() {
var mu1, mu2 sync.Mutex
go func() {
mu1.Lock()
mu2.Lock() // ждёт mu2
mu2.Unlock()
mu1.Unlock()
}()
mu2.Lock()
mu1.Lock() // ждёт mu1, который удерживает первая горутина
mu1.Unlock()
mu2.Unlock()
}
// Правильный подход: таймаут через select
func safeSend(ch chan int, value int) bool {
select {
case ch <- value:
return true
case <-time.After(time.Second):
return false
}
}

Как отвечать на собеседовании

Начните с чёткого определения дедлока и условий его возникновения (взаимное исключение, удержание и ожидание, отсутствие вытеснения, циклическое ожидание). Затем объясните, как Go runtime детектирует дедлоки - только когда все горутины заблокированы. Приведите конкретные примеры из практики: каналы, мьютексы, WaitGroup. Обязательно упомяните, что частичные дедлоки не детектируются, и это серьёзная проблема в production. Расскажите о методах предотвращения: таймауты, единый порядок блокировок, контексты. Если спросят про инструменты - упомяните go vet, go race detector (он не ловит дедлоки, но помогает найти гонки данных, которые часто сопутствуют дедлокам). Покажите, что понимаете разницу между дедлоком, livelock и starvation.

Что проверяет интервьюер

Интервьюер проверяет:

  • Понимание модели конкурентности в Go (горутины, каналы, мьютексы).
  • Способность анализировать конкурентный код и находить потенциальные дедлоки.
  • Знание ограничений runtime-детектора дедлоков.
  • Практические навыки предотвращения дедлоков.
  • Умение объяснять сложные концепции простыми словами.
  • Понимание trade-off между использованием каналов и мьютексов.

Для senior-позиции важно показать не только знание теории, но и реальный опыт отладки дедлоков в production, умение работать с дампом горутин (SIGQUIT или pprof).

Типичные ошибки

  • Утверждение, что Go полностью защищает от дедлоков - это не так.
  • Непонимание, что runtime детектирует только полные дедлоки, когда все горутины заблокированы.
  • Предложение использовать go run -race как средство обнаружения дедлоков - race detector не ловит дедлоки.
  • Забывание, что мьютексы в Go не реентерабельны.
  • Использование time.Sleep как способа "решить" проблему дедлока - это маскировка, а не решение.
  • Неумение читать стек-трейсы при фатальной ошибке дедлока.
  • Игнорирование возможности дедлока при работе с sync.WaitGroup внутри циклов.
  • Путаница между дедлоком и обычной блокировкой на канале, которая разрешится позже.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью