Перед нами код на Go, который использует горутины и каналы для вычисления суммы квадратов. Для начала попробуем ответить на вопрос: что выведет программа и почему? А затем разберемся, как исправить все имеющиеся проблемы.
- Исходный код
- Что происходит в исходном коде?
- Проблема №1. Взаимная блокировка (Deadlock)
- Что происходит:
- Как проявляется:
- Проблема №2. Незакрытый канал
- Проблема №3. Несинхронизированное взаимодействие
- Как можно исправить?
- 1. Запуск получателя до ожидания
- 2. Использование буферизованного канала
- 3. Полное разделение ответственности
- Ключевые выводы
Исходный код
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int)
wg := &sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
go func(v int) {
defer wg.Done()
ch <- v * v
}(i)
}
wg.Wait()
var sum int
for v := range ch {
sum += v
}
fmt.Printf("result: %d\n", sum)
}
Что происходит в исходном коде?
Сначала проанализируем, что пытается сделать программа:
- Создается небуферизованный канал
ch := make(chan int) - Запускаются 3 горутины, каждая из которых:
- Вычисляет квадрат чисел
0, 1 и 2 - Отправляет результат в канал
- Вызывает
wg.Done()для уменьшения счетчикаWaitGroup
- Вычисляет квадрат чисел
- Главная горутина ждет завершения всех горутин через
wg.Wait() - Пытается прочитать все значения из канала с помощью
range ch - Суммирует значения и выводит результат
Проблема №1. Взаимная блокировка (Deadlock)
Это главная и самая критическая проблема.
ch := make(chan int) // небуферизованный канал
Что происходит:
Небуферизованный канал требует одновременной готовности отправителя и получателя. В нашем случае:
- Горутины-отправители пытаются отправить данные в канал:
ch <- v * v - Главная горутина в это время заблокирована на
wg.Wait()и не читает из канала - Результат: 3 горутины заблокированы на операции отправки, а главная горутина заблокирована на ожидании.
Как проявляется:
fatal error: all goroutines are asleep - deadlock!
Проблема №2. Незакрытый канал
Даже если бы не было взаимной блокировки, есть вторая проблема:
for v := range ch {
sum += v
}
Конструкция range ch будет читать из канала до тех пор, пока канал не будет закрыт. Но в коде нет вызова close(ch), поэтому цикл будет вечно ждать новые данные даже после получения всех 3-х значений.
Проблема №3. Несинхронизированное взаимодействие
В коде пытаются использовать WaitGroup для синхронизации завершения горутин, но не синхронизируется сам процесс обмена данными через канал.
Как можно исправить?
Рассмотрим 3 способа, как это можно исправить.
1. Запуск получателя до ожидания
Получатель (range по каналу) запускается в главной горутине ДО того, как отправлены все данные. При этом используется отдельная горутина, которая закрывает канал сразу после того, как все отправители завершили работу.
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int)
wg := &sync.WaitGroup{}
wg.Add(3)
// запускаем горутины для отправки данных
for i := 0; i < 3; i++ {
go func(v int) {
defer wg.Done()
ch <- v * v
}(i)
}
// запускаем отдельную горутину для закрытия канала
go func() {
wg.Wait()
// закрываем канал после завершения всех отправителей
close(ch)
}()
// читаем из канала в главной горутине
var sum int
for v := range ch {
sum += v
}
fmt.Printf("result: %d\n", sum)
// result: 5 (0 + 1 + 2 = 0² + 1² + 4² = 5)
}
2. Использование буферизованного канала
Создаётся канал с буфером, куда помещаются все данные без блокировки отправителей (пока буфер не заполнится). После того как все отправители завершились, канал закрывается в главной горутине, и затем происходит чтение.
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 3) // буфер на 3 элемента
wg := &sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
go func(v int) {
defer wg.Done()
ch <- v * v
}(i)
}
wg.Wait()
// важно закрыть канал после завершения всех отправок
close(ch)
var sum int
for v := range ch {
sum += v
}
fmt.Printf("result: %d\n", sum) // result: 5
}
3. Полное разделение ответственности
Отправители, получатель и синхронизация живут отдельно. Получатель запущен в своей горутине, отправители — в своих. Есть отдельный канал done, сигнализирующий о том, что получатель закончил работу.
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int)
done := make(chan struct{})
// горутина-получатель
go func() {
var sum int
for v := range ch {
sum += v
}
fmt.Printf("result: %d\n", sum)
close(done)
}()
// горутины-отправители
wg := &sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
go func(v int) {
defer wg.Done()
ch <- v * v
}(i)
}
// синхронизация
wg.Wait()
// закрываем канал данных
close(ch)
// ждем завершения получателя
<- done
}
Ключевые выводы
- Небуферизованные каналы требуют одновременной готовности отправителя и получателя.
- Всегда закрывайте каналы со стороны отправителя, когда используете
range. WaitGroupхорош для синхронизации завершения работы, но не для синхронизации доступа к данным.- Порядок операций критически важен при работе с каналами.
- Используйте буферизованные каналы, когда отправители и получатели работают асинхронно.
Правильное использование горутин и каналов — ключ к написанию эффективных и надежных concurrent-программ на Go.
Напишите в комментариях — как вы обнаружили проблему и какой способ решения оказался самым эффективным? Может быть, у вас есть свои приемы для избежания deadlock в горутинах.