🔃

Goのデッドロックを理解する:バッファ無しチャネルの落とし穴

に公開

次のようなコードを書くとデッドロックが発生します。

package main

import "fmt"

func main() {
  ch := make(chan int)
  // チャネルに送信
  ch <- 1
    
  // チャネルから受信
  fmt.Println(<-ch)
}

実行結果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /××××××/main.go:7 +0x38
exit status 2

なぜデッドロックが起きるのか?

ここで作られている chバッファ無しチャネル です。

バッファ無しチャネルでは、

  • 送信 ch <- v
  • 受信 v := <-ch

両方が同時に揃わない限り処理が進みません

つまり、送信側は「受信者が準備できるまで永久に待つ」動作になります。

このコードでは main ゴルーチンしか存在しないため、

  1. ch <- 1 で送信しようとする
  2. 受信してくれる相手がまだいないため main が停止
  3. main が止まったので、その後の fmt.Println(<-ch) に到達できない
  4. 全ゴルーチン(= main)が待ち状態

という流れになります。

デッドロックを回避するには

送信と受信を別ゴルーチンで実行し、両方が同時に成立できるようにします。

package main

import "fmt"

func main() {
  ch := make(chan int)

  go func() {
    ch <- 1 // 別ゴルーチンで送信
  }()

  fmt.Println(<-ch) // main で受信
}

これなら、

  • main が受信で待つ
  • 別ゴルーチンが送信してくれる

という状態が作れるので、デッドロックにはなりません。

注意

逆に次のコードはデッドロックになります。

package main

import "fmt"

func main() {
  ch := make(chan int)
  // チャネルに送信
  ch <- 1

  go func() {
    // チャネルから受信
    fmt.Println(<-ch)
  }()
}

理由:

  • ch <- 1 の時点で main がブロックしてしまい、
  • go func(){} の行に永遠に到達しないため、受信ゴルーチンが起動せずデッドロックが発生します。

つまり 順番の問題 で送受信が成立しません。

(補足)バッファ付きチャネルなら成立する

ch := make(chan int, 1)

とすると、

ch <- 1 はバッファに入ってブロックされないので、下のゴルーチンが後から読み出してもデッドロックしません。

Discussion