Что такое блокировка (deadlock) при работе с каналами в Go и как она возникает?

Ответ

Блокировка (deadlock) в Go — это ситуация, когда одна или несколько горутин бесконечно ожидают события, которое никогда не произойдет. Чаще всего это связано с неправильной работой с каналами.

Основные причины блокировок с каналами:

  1. Отправка в небуферизованный канал без получателя. Небуферизованный канал (make(chan T)) требует, чтобы отправитель и получатель встретились одновременно. Если горутина пытается отправить данные, а получателя нет, она блокируется навсегда.

    func main() {
        ch := make(chan int)
        ch <- 1 // Deadlock! Нет другой горутины, готовой принять значение.
    }
  2. Отправка в заполненный буферизованный канал. Буферизованный канал (make(chan T, N)) блокирует отправку только тогда, когда его буфер полон, и нет готового получателя.

    func main() {
        ch := make(chan int, 1) // Буфер размером 1
        ch <- 1 // OK, значение помещено в буфер
        ch <- 2 // Deadlock! Буфер полон, и никто не читает из канала.
    }
  3. Чтение из пустого канала, который никто не закроет. Горутина, читающая из канала, будет заблокирована, пока в него не придут данные или пока канал не будет закрыт.

Способы избежать блокировки:

  • Запускать отправителей и получателей в разных горутинах. Это основной паттерн конкурентной работы в Go.
  • Использовать select с default для неблокирующей отправки/чтения:
    select {
    case ch <- value:
        // Успешно отправлено
    default:
        // Канал заблокирован, выполняем альтернативное действие
    }
  • Использовать select с таймаутом, чтобы избежать вечного ожидания:
    select {
    case val := <-ch:
        // Получили значение
    case <-time.After(1 * time.Second):
        // Не дождались за 1 секунду
    }
  • Правильно закрывать каналы. Канал закрывается отправителем, чтобы сообщить получателям, что данных больше не будет.

Ответ 18+ 🔞

А, слушай, смотри, вот эта штука — deadlock в Go — это когда твои горутины тупо встали как вкопанные и ждут друг от друга какого-то пиздеца, который никогда не случится. В основном, конечно, из-за каналов косячат, потому что с ними как с огнём — чуть не туда, и всё, приехали.

Основные причины, почему всё накрывается медным тазом:

  1. Пытаешься запихнуть в небуферизованный канал, а читать некому.
    Небуферизованный канал (make(chan int)) — это как встреча в подворотне: чтобы передать пачку, нужно, чтобы оба были на месте. Если ты суёшь значение, а получателя нет — всё, ты стоишь там до скончания века, как лох.

    func main() {
        ch := make(chan int)
        ch <- 1 // Ёбта, deadlock! Кто это принимать-то будет? Никого же!
    }
  2. Забил буферизованный канал под завязку, а выгребать не начали.
    Буферизованный (make(chan int, 1)) — тут уже есть маленькая коробочка на одно значение. Засунул одно — ок. Пытаешься второе запихнуть, а коробка-то полная, и никто не освобождает — опять стоишь, как мудак.

    func main() {
        ch := make(chan int, 1)
        ch <- 1 // Ну вроде норм, лёг в буфер
        ch <- 2 // А вот тут пиздец! Буфер забит, а читателей нет. Завис.
    }
  3. Ты пялишься в пустой канал, который никто и никогда не закроет.
    Читаешь из канала, а там пустота, и никто не собирается туда ничего слать. И закрывать не собирается. Ну и стоишь, блядь, до пенсии.

Как не облажаться:

  • Запускай отправителей и получателей в разных горутинах. Это же основа основ, ёпта! Один пишет, другой читает — красота.
  • Юзай select с default — это как попытка сунуть, а если не лезет — похуй, идём дальше:
    select {
    case ch <- value:
        // Ура, протолкнул!
    default:
        // Канал не готов, ну и хуй с ним, делаем что-то ещё
    }
  • select с таймаутом — чтобы не ждать вечно, как последний лох:
    select {
    case val := <-ch:
        // О, пришло!
    case <-time.After(1 * time.Second):
        // Ждал-ждал, нихуя. Пиздуй дальше.
    }
  • Закрывай каналы правильно. Закрывает всегда отправитель, типа "всё, братан, больше ничего не будет". Получатель это видит и не тупит вечно.