Go

Go言語のChannelは送信時にもブロックする

More than 3 years have passed since last update.


TL;DR

channelからの受信時に処理がブロックするのは有名だけど、送信時にブロックするのは忘れがちだから気を付けましょう、というお話。


現象

channelからの受信は完了するまでブロックするというのは有名かと思います。この仕様を利用して同期処理書いたりしますしね。

package main

import "fmt"

func greatFunction(ch chan<- string) {
// 何かすごい処理
ch <- "何かすごい処理結果"
}

func main() {
ch := make(chan string)
go greatFunction(ch)
fmt.Println(<-ch) // ここで処理結果が出るまでブロックする
}

でも、channelがブロックするのは受信時だけではありません。忘れがちですが送信時にもブロックすることがあります。

その結果、無慈悲なデッドロックに遭遇してしまったコードがこちらです。


server.go

package msqsrv

import "net"

func MsqServer(msq chan<- string) {
listener, _ := net.Listen("tcp", "localhost:12345")
conn, _ := listener.Accept()
buf := make([]byte, 255)
readLen, _ := conn.Read(buf)
msq <- string(buf[:readLen])
conn.Write([]byte("メッセージ受け取ったよ"))
}



server_test.go

package msqsrv

import (
"net"
"testing"
)

func TestMsqServer(t *testing.T) {
msq := make(chan string, 10)
go MsqServer(msq)

conn, _ := net.Dial("tcp", "localhost:12345")
conn.Write([]byte("わかめ"))
buf := make([]byte, 255)
conn.Read(buf)

if <- msq != "わかめ" {
t.Error("error!")
}
}


色々と適当ですが、上記はstring型のchannelをメッセージキューと見立てて、受信メッセージをキューに放り込むサーバとそのテストです。サーバは処理終了後にクライアントへ受信完了のメッセージを返します。

そして、残念ながらこのテスト実行時にはデッドロックが発生します。


何で?

channelへのデータ送信時にchannelのサイズを超過すると、超過分のデータがchannelから読み出されるまで送信処理がブロックするからです。

なお、サイズ指定を省略してchannelをmakeした場合、そのサイズは0です。

一方でnetパッケージのConn#Readは相手側でWriteされるまでブロックするため、デッドロックが発生してしまったのですね。

// server.goから抜粋

func MsqServer(msq chan<- string) {
listener, _ := net.Listen("tcp", "localhost:12345")
conn, _ := listener.Accept()
buf := make([]byte, 255)
readLen, _ := conn.Read(buf)
msq <- string(buf[:readLen]) // <- ここでブロックしてしまう
conn.Write([]byte("メッセージ受け取ったよ")) // <- ここに辿り着けない
}

// server_test.goから抜粋
func TestMsqServer(t *testing.T) {
msq := make(chan string, 10)
go MsqServer(msq)

conn, _ := net.Dial("tcp", "localhost:12345")
conn.Write([]byte("わかめ"))
buf := make([]byte, 255)
conn.Read(buf) // <- MsgServer関数でWriteされるまでブロックする

if <- msq != "わかめ" {
t.Error("error!")
}
}

このデッドロックを回避するにはmsqからの受信処理(テスト関数内のアサーション部分)をRead関数の呼び出しよりも先に持ってくるか、channel生成時に十分なサイズを割り当てます。

先述の例では同時に複数のデータが送受信されることは無いので、以下の様にサイズ1を割り当てればよいでしょう。

msq := make(chan string, 1)

チャネル生成時にはサイズにも気を配りましょう、というお話ですね。

(それ以前にロック順にも注意すべきですが・・・)


不安

というか、忘れてたの僕だけ?