TL;DR

  • デッドロックは Mutex だけの問題ではなく、チャネルでも起こりうる。
  • ゴルーチンがあるチャネルを使いながら別のチャネルを使おうとすると、デッドロックになる可能性がある。

内容

ゴルーチンが排他的なリソースを保持したまま他のリソースを要求すると、デッドロックが起こりうる(Coffman conditions)。これは Mutex に限った話ではなく、チャネルでも起こる。

チャネルの容量は、読み込みリソースと書き込みリソースの集まりという排他的なリソースと考えられる。バッファなしチャネルでは、初期状態ではどちらのリソースもない。あるゴルーチンが書き込もうとすると、書き込みリソースを獲得しようとする間に、読み込みリソースを 1 つ利用可能にする。逆に読み込もうとすると、読み込みリソースを獲得しようとする間に、書き込みリソースを 1 つ利用可能にする。そのため、ゴルーチンがあるチャネルを保持しながら、別のチャネルでメッセージを送受信しようとすると、デッドロックが起こりうる。

次の例では、handleDirectories が dirs から受け取ったディレクトリ内のファイルを files に送り、handleFiles が files から受け取ったパスがディレクトリなら dirs に送り返す。互いに相手のチャネルへ送信しようとして待ち続けると、デッドロックになる。

func handleDirectories(dirs <-chan string, files chan<- string) {
	for fullpath := range dirs {
		fmt.Println("Reading all files from", fullpath)
		filesInDir, _ := os.ReadDir(fullpath)
		fmt.Printf("Pushing %d files from %s\n", len(filesInDir), fullpath)
		for _, file := range filesInDir {
			files <- filepath.Join(fullpath, file.Name())
		}
	}
}
 
func handleFiles(files <-chan string, dirs chan<- string) {
	for path := range files {
		file, _ := os.Open(path)
		fileInfo, _ := file.Stat()
		if fileInfo.IsDir() {
			fmt.Printf("Pushing %s directory\n", fileInfo.Name())
			dirs <- path
			return
		}
 
		fmt.Printf("File %s, size: %.2fKB, last modified: %s\n",
			fileInfo.Name(), float64(fileInfo.Size())/1024.0,
			fileInfo.ModTime().Format(time.ANSIC))
	}
}

キーワード