Что происходит с горутиной при чтении из пустого канала?

Ответ

Когда горутина пытается прочитать данные из пустого канала, она блокируется.

Это ключевой механизм синхронизации в Go. Вот что происходит на более низком уровне:

  1. Переход в состояние ожидания: Горутина, выполняющая блокирующую операцию чтения (<-myChannel), переходит в состояние waiting.
  2. Освобождение потока ОС: Планировщик Go (Go scheduler) снимает эту горутину с выполнения и убирает ее из очереди готовых к выполнению (runnable) горутин. Поток операционной системы (M), на котором она выполнялась, освобождается и может быть использован для выполнения другой runnable горутины.
  3. Экономия CPU: Благодаря этому механизму, ожидающая горутина не потребляет ресурсы CPU (в отличие от активного ожидания в цикле).
  4. Пробуждение: Горутина будет оставаться в состоянии waiting до тех пор, пока другая горутина не отправит данные в этот канал. Как только данные появятся, планировщик переместит заблокированную горутину обратно в очередь runnable, и она сможет продолжить свое выполнение, прочитав отправленное значение.

Пример блокирующего чтения:

func main() {
    ch := make(chan int) // Создаем небуферизованный канал

    go func() {
        fmt.Println("Горутина начала работу, сейчас заснет...")
        val := <-ch // <-- Горутина блокируется здесь
        fmt.Printf("Горутина проснулась и получила значение: %dn", val)
    }()

    time.Sleep(2 * time.Second) // Даем время горутине запуститься и заблокироваться
    fmt.Println("Основная горутина сейчас отправит данные в канал.")
    ch <- 10 // <-- Отправка данных разблокирует другую горутину
    time.Sleep(1 * time.Second) // Ждем, чтобы горутина успела вывести сообщение
}

Важные случаи:

  • Чтение из закрытого канала: Если канал был закрыт (close(ch)) и в нем больше нет данных, операция чтения не будет блокироваться. Она немедленно вернет нулевое значение для типа канала и false в качестве второго параметра.

    val, ok := <-ch // Если ch закрыт и пуст, вернется (0, false) для chan int

    Цикл for range над каналом в этом случае просто завершится.

  • Неблокирующее чтение: Чтобы избежать блокировки, можно использовать конструкцию select с веткой default.

    select {
    case val := <-ch:
        fmt.Printf("Получено значение: %vn", val)
    default:
        fmt.Println("Канал пуст, выполняем другие действия.")
        // Блокировки не произошло
    }