Почему этот простой код на Go зависнет навсегда?

deadlock go

Если вы запустите этот код на 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!

Давайте разберемся, почему это происходит и как это исправить.

Что происходит в данном коде?

package main

import "fmt"

func main() {
    ch := make(chan int) // 1. Создаем небуфиризованный канал
    ch <- 1              // 2. Пытаемся отправить значение в канал
    go func() {          // 3. Запускаем горутину
        fmt.Println(<-ch)// 4. Пытаемся прочитать из канала
    }()
}

Почему происходит блокировка?

  1. Небуферизованный канал: make(chan int) создает канал без буфера.
  2. Синхронность операций: Операции отправки (ch <- 1) и получения (<-ch) в небуферизованном канале являются блокирующими до тех пор, пока не встретится соответствующая операция с другой стороны.
  3. Последовательность выполнения: 
    1. Строка ch <- 1 блокируется, потому что нет готовой горутины для чтения из канала
    2. Горутина создается только после этой блокировки, поэтому она никогда не запустится

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

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

Способ 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() // ждем завершения горутины
}

Ключевые выводы:

  1. Небуферизованные каналы синхронны — отправка блокируется до появления получателя, и наоборот.
  2. Порядок имеет значение — при работе с небуферизованными каналами важно, чтобы получатель был готов до или одновременно с отправителем.
  3. Основная горутина — если основная горутина блокируется до запуска других горутин, программа может войти в состояние deadlock.
  4. Синхронизация — для корректной работы конкурентных программ нужна правильная синхронизация между горутинами.

А вы сталкивались с deadlock’ами в Go? Поделитесь в комментариях: какой был самый хитрый случай взаимоблокировки, который вам приходилось отлаживать? Возможно, ваш опыт поможет другим разработчикам избежать подобных ловушек.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: