Если вы запустите этот код на Go, то он ничего не выведет и просто заблокируется навсегда (deadlock).
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
go func() {
fmt.Println(<-ch)
}()
}
fatal error: all goroutines are asleep — deadlock!
Давайте разберемся, почему это происходит и как это исправить.
Содержание
- Что происходит в данном коде?
- Почему происходит блокировка?
- Как исправить?
- Способ 1: Запустить горутину перед отправкой (наиболее правильный)
- Способ 2: Использовать буферизованный канал
- Способ 3: Использовать отдельную горутину для отправки
- Способ 4: Использовать sync.WaitGroup для синхронизации
- Ключевые выводы:
Что происходит в данном коде?
package main
import "fmt"
func main() {
ch := make(chan int) // 1. Создаем небуфиризованный канал
ch <- 1 // 2. Пытаемся отправить значение в канал
go func() { // 3. Запускаем горутину
fmt.Println(<-ch)// 4. Пытаемся прочитать из канала
}()
}
Почему происходит блокировка?
- Небуферизованный канал:
make(chan int)создает канал без буфера. - Синхронность операций: Операции отправки
(ch <- 1)и получения(<-ch)в небуферизованном канале являются блокирующими до тех пор, пока не встретится соответствующая операция с другой стороны. - Последовательность выполнения:
- Строка
ch <- 1блокируется, потому что нет готовой горутины для чтения из канала - Горутина создается только после этой блокировки, поэтому она никогда не запустится
- Строка
Программа попадает в тупик: основная горутина ждет, пока кто-то прочитает из канала, но горутина, которая должна читать, никогда не запускается из-за блокировки.
Как исправить?
Способ 1: Запустить горутину перед отправкой (наиболее правильный)
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
fmt.Println(<-ch) // готов читать
}()
ch <- 1 // теперь есть кто-то, кто примет значение
}
Способ 2: Использовать буферизованный канал
package main
import "fmt"
func main() {
ch := make(chan int, 1) // буфер на 1 элемент
ch <- 1 // отправка не блокируется (буфер свободен)
go func() {
fmt.Println(<-ch) // читаем из канала
}()
// нужно добавить ожидание, чтобы горутина успела выполниться
time.Sleep(time.Millisecond * 100
}
Способ 3: Использовать отдельную горутину для отправки
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
ch <- 1 // отправляем из горутины
}()
go func() {
fmt.Println(<-ch) // читаем из другой горутины
}()
time.Sleep(time.Millisecond * 100)
}
Способ 4: Использовать sync.WaitGroup для синхронизации
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(<-ch)
}()
ch <- 1
wg.Wait() // ждем завершения горутины
}
Ключевые выводы:
- Небуферизованные каналы синхронны — отправка блокируется до появления получателя, и наоборот.
- Порядок имеет значение — при работе с небуферизованными каналами важно, чтобы получатель был готов до или одновременно с отправителем.
- Основная горутина — если основная горутина блокируется до запуска других горутин, программа может войти в состояние
deadlock. - Синхронизация — для корректной работы конкурентных программ нужна правильная синхронизация между горутинами.
А вы сталкивались с deadlock’ами в Go? Поделитесь в комментариях: какой был самый хитрый случай взаимоблокировки, который вам приходилось отлаживать? Возможно, ваш опыт поможет другим разработчикам избежать подобных ловушек.