2010年9月14日火曜日

deferを使うときの注意

2010/09/25 追記

こっちのやり方のほうがいいかも

Goでコードを書いてるとdeferを積極的に使いたくなるんだけど、それで調子に乗ってdeferしまくってたら罠にはまった。

具体的には、あるバージョンからstring.Splitの仕様が変わって、それに気付かず古いコードのままでコンパイルして実行したらある地点から処理が進まなくなった。でしばらく調査してわかったのが、処理が進まなくなったのはpanicが発生してdeferが意図したタイミングでないときに走り、結果として処理がブロックしていたからということ。

deferのなかでブロックする

検証用にサンプルソースを用意した。重要なのは func process()のなか。panicを起こさなければ正常終了するけれど、panicを起こさせると全スレッドがデッドロックを起こす。


package main

import (
    "log"
    "os"
)

var logger *log.Logger = log.New(os.Stdout, nil , "", log.Lok)

type Message int
type Input int
type Output int

type InputWithResponse struct {
    data Input
    responseChannel chan Output
}

const (
    MSG_FINISHED Message = 0
    MSG_ABORT Message = 1
)

const (
    OUT_DATA Output = 0
)

const (
    IN_DATA Input = 0
)


func process(input InputWithResponse) {
    logger.Log("Processing input:", input.data)

    // we simulate a panic here.
    panic("process()")

    // send back the processed result
    input.responseChannel <- OUT_DATA
}

func Processor(msgChannel chan Message, inputChannel chan InputWithResponse) {
    // send a message when we're done
    defer func() {
        msgChannel <- MSG_FINISHED
    }()

    run := true
    for run {
        select {
        case msg := <- msgChannel:
            switch msg {
            case MSG_ABORT:
                logger.Log("ABORT message received.")
                run = false
            }
        case input := <- inputChannel:
            logger.Log("Received input:", input)
            process(input)
        }
    }
    logger.Log("Finished processing.")
}

func main() {
    msgChannel := make(chan Message) // create a message channel
    inputChannel := make(chan InputWithResponse) // create a I/O channel
    logger.Log("Start processing.")
    go Processor(msgChannel, inputChannel)

    outputChannel := make(chan Output)
    input := InputWithResponse {
        IN_DATA,
        outputChannel,
    }

    inputChannel <- input
    output := <- outputChannel // get processed data
    logger.Log("Processed output:", output)

    msgChannel <- MSG_ABORT // stop the processor

    _ = <- msgChannel // wait for the processor to end

    logger.Log("Exiting.")
}

process()のなかでpanic()を呼ばなかった場合の出力

Start processing.
Received input: {0 0xb77ad780}
Processing input: 0
Processed output: 0
ABORT message received.
Finished processing.
Exiting.

process()のなかでpanicした場合の出力

Start processing.
Received input: {0 0xb761b780}
Processing input: 0
throw: all goroutines are asleep - deadlock!

panic PC=0xb761e080
throw+0x49 /home/masato/lib/google-go/src/pkg/runtime/runtime.c:73
        throw(0xffffffff, 0x80993af)
nextgandunlock+0x19a /home/masato/lib/google-go/src/pkg/runtime/proc.c:329
        nextgandunlock()
scheduler+0x158 /home/masato/lib/google-go/src/pkg/runtime/proc.c:521
        scheduler()
mstart+0x75 /home/masato/lib/google-go/src/pkg/runtime/proc.c:389
        mstart()
clone+0x90 /home/masato/lib/google-go/src/pkg/runtime/linux/386/sys.s:198
        clone()
(以下略)

デッドロックが起きる理由というのが、deferした関数がpanicの直後に処理されるから。流れとしては下のような感じ。

  1. panicを起こすと、その時点でprocess()の実行はreturnする
  2. するとprocess()のなかで起きるはずだったresponseChannelへの書き込みが発生しない
  3. 結果main()がresponseChannelの返答を待っているところでブロックする
  4. また、panicが上位に伝播する過程でProcessor()のdeferが実行され、そこでもmsgChannelへの書きこみがブロックする
  5. msgChannelから読むはずのmain()はresponseChannelの返答をずっと待っている
  6. デッドロック完成!

デッドロックさせないためには

試したところ以下の方法が有効だった。
  • deferの先頭周辺(ブロックしてしまう処理の前)にrecover()を挿入する
  • msgChannelをbuffered channelに置きかえる
  • process()をgoroutineとして実行する

recover()を挿入する

recover()を挿入するのがおそらく最も正しいやり方。こうすることで意図しないエラーが発生した場合どうするかを明示的に書くことができる。具体的には、deferする関数を下のように書きかえれば、デッドロックでは無く通常のpanicとして異常終了させられる。


defer func() {
    if x := recover(); x != nil {
        panic(x) // go back to panicking
    }
    msgChannel <- MSG_FINISHED
}()

ドキュメントで言うとこのへんに詳細が記述されている。


buffered channelに置きかえる

msgChannelをmakeするところで、"make(chan Message, 5)"のようにバッファつきのチャンネルを作ればdeferのなかでブロックすることも無くなるので、recover()しなくても上位にpanicが伝播する。

process()をgoroutineとして実行する

どうもgoroutineとして実行された関数は、panicを親ルーチンへ伝播させるわけではなく、いきなりもっと上位まで伝播させるようになるらしい。つまり、"process(input)"と記述されているところを"go process(input)"と変えるだけでprocess()内のpanicが最上位まで伝播するようになる、ということ。

processをgoroutineとして実行した場合の出力

Start processing.
Received input: {0 0xb77ad780}
Processing input: 0
panic: process()

panic PC=0xb7791150
runtime.panic+0xa9 /home/masato/lib/google-go/src/pkg/runtime/proc.c:1015
        runtime.panic(0x0, 0xb7791180)
main.process+0xd8 /home/masato/desk/programs/go/lock-test.go:40
        main.process(0x809d268, 0xb778c870)
goexit /home/masato/lib/google-go/src/pkg/runtime/proc.c:145
        goexit()
(以下略)

まとめ

ということで、Goでdeferを使うときは下位の関数が起こすかもしれないpanicに注意しましょう。なお今回のサンプルをビルドするのにつかったGoのリビジョンはr6255:1dc78bb51937でした。

  • deferした関数の中では極力ブロックしない
  • 今回の例のようにdeferのなかでchannelへの書き込みをする場合、make(chan ****, 5)とかでbuffered channelを作って、ブロックしないようにする
  • どうしてもdeferのなかでブロックする場合は、ブロックする箇所より前にrecover()を入れて下位で発生したpanicに備える
  • もしくは、panicが起きそうな処理は別のgoroutineとして実行し、上位までpanicが伝播するようにしておく

0 件のコメント:

コメントを投稿