← Назад к вопросам

Найти баг: deadlock в select

1.8 Middle🔥 191 комментариев
#Основы Go

Условие

Найдите проблему deadlock в следующем коде:

func FindMaxProblem1() {
    var wg sync.WaitGroup
    ch := make(chan string, 5)
    mu := sync.Mutex{}
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(ch chan<- string, i int, grp *sync.WaitGroup) {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()
            msg := fmt.Sprintf("Goroutine %s", strconv.Itoa(i))
            ch <- msg
        }(ch, i, &wg)
    }
    for {
        select {
        case q := <-ch:
            fmt.Println(q)
        }
    }
    wg.Wait()
}

Вопросы

  1. Почему возникает deadlock?
  2. Как исправить код?
  3. Нужен ли здесь мьютекс?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Найти баг — deadlock в select

Проблема 1: Бесконечный select без default

Основная проблема — бесконечный for цикл с select:

for {
    select {
    case q := <-ch:
        fmt.Println(q)
    }
}

Этот код никогда не завершится, потому что:

  • Все 5 горутин выполнены и заблокированы в mu.Lock()
  • Основной поток вечно ждёт данные из ch
  • wg.Wait() никогда не достигается

Deadlock: основной поток ждёт горутины, но горутины не могут завершиться (ждут отправки в канал, который основной поток не успевает читать), а основной поток не может закончить читать, потому что for бесконечен.

Проблема 2: Неправильная логика синхронизации

Все горутины конкурируют за один mu.Mutex, но в буферизированном канале нет необходимости синхронизировать отправку. Мьютекс создаёт дополнительные задержки и усложняет диагностику проблем.

Исправленный вариант 1: С сигналом завершения

func FindMaxFixed1() {
    var wg sync.WaitGroup
    ch := make(chan string, 5)
    
    // Запускаем горутины (мьютекс не нужен)
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(ch chan<- string, i int) {
            defer wg.Done()
            msg := fmt.Sprintf("Goroutine %d", i)
            ch <- msg
        }(ch, i)
    }
    
    // Закрываем канал после завершения всех горутин
    go func() {
        wg.Wait()
        close(ch)
    }()
    
    // Читаем из канала до его закрытия
    for q := range ch {
        fmt.Println(q)
    }
}

Как работает:

  • Горутины пишут в канал и завершаются
  • Отдельная горутина ждёт wg.Wait(), потом закрывает канал
  • for range ch автоматически завершается при закрытии канала
  • Deadlock исчезает

Исправленный вариант 2: С контролем количества читаемых элементов

func FindMaxFixed2() {
    var wg sync.WaitGroup
    ch := make(chan string, 5)
    
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(ch chan<- string, i int) {
            defer wg.Done()
            msg := fmt.Sprintf("Goroutine %d", i)
            ch <- msg
        }(ch, i)
    }
    
    // Читаем ровно 5 результатов
    for i := 0; i < 5; i++ {
        q := <-ch
        fmt.Println(q)
    }
    
    wg.Wait()
}

Как работает:

  • Знаем точное количество горутин (5)
  • Читаем ровно 5 элементов из канала
  • После этого выходим из цикла
  • wg.Wait() гарантирует завершение горутин

Исправленный вариант 3: С default case в select

func FindMaxFixed3() {
    var wg sync.WaitGroup
    ch := make(chan string, 5)
    
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(ch chan<- string, i int) {
            defer wg.Done()
            msg := fmt.Sprintf("Goroutine %d", i)
            ch <- msg
        }(ch, i)
    }
    
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()
    
    for {
        select {
        case q := <-ch:
            fmt.Println(q)
        case <-done:
            return
        }
    }
}

Как работает:

  • Используем отдельный канал done для сигнала завершения
  • select получает сигнал и выходит из функции
  • Более явный контроль над окончанием

Нужен ли здесь мьютекс?

Нет, по следующим причинам:

  1. Канал буферизирован (make(chan string, 5))

    • Горутины не блокируют друг друга при отправке
    • Можно отправить до 5 сообщений без синхронизации
  2. Нет общего состояния

    • Каждая горутина независима
    • Не изменяем общие переменные
  3. Каналы — синхронизация

    • Go рекомендует передавать данные по каналам, а не синхронизировать через мьютексы
    • "Do not communicate by sharing memory; share memory by communicating"
  4. Мьютекс замораживает все горутины

    • Когда одна горутина в критической секции, остальные ждут
    • Это противоречит параллелизму

Краткие ответы

1. Почему deadlock?

  • Основной поток вечно ждёт в select на строке q := <-ch
  • Горутины завершены, но wg.Wait() никогда не выполнится
  • Программа зависает

2. Как исправить?

  • Закрывать канал после завершения горутин (close(ch))
  • Использовать for range ch или считать элементы
  • Или сигнализировать завершение отдельным каналом

3. Нужен ли мьютекс?

  • Нет. Буферизированный канал справляется с синхронизацией
  • Мьютекс усложняет код и замораживает параллелизм

Это типичная ошибка при работе с каналами и горутинами в Go.