生涯未熟

生涯未熟

プログラミングをちょこちょこと。

runtime.Goexitを改めて理解する

序文

runtime packageには Goexit() というfunctionがあります。

// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.

// Goexitはそれを呼び出すgoroutineを終了します。 その他のゴルーチンは影響を受けません。
// Goexitは、goroutineを終了する前にすべてのdefer呼び出しを実行します。 Goexitはpanicではないので、これらのdefer関数の中のすべてのrecover呼び出しはnilを返します。
// メインのgoroutineからGoexitを呼び出すと、メインgoroutineに戻ることなくそのgoroutineが終了します。 func mainは返されていないので、プログラムは他のgoroutineの実行を続けます。
// その他のゴルーチンがすべて終了すると、プログラムがクラッシュします。
func Goexit() {
    // Run all deferred functions for the current goroutine.
    // This code is similar to gopanic, see that implementation
    // for detailed comments.

        // 現在のゴルーチンのすべてのdefer関数を実行します。
        // このコードはgopanicに似ています。詳細なコメントの実装を参照してください。
    gp := getg()
    for {
        d := gp._defer
        if d == nil {
            break
        }
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
            continue
        }
        d.started = true
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        if gp._defer != d {
            throw("bad defer entry in Goexit")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        // Note: we ignore recovers here because Goexit isn't a panic
    }
    goexit1()
}

コメントにあるように、以下の特性を持ちます。

  • 呼び出し元のgoroutineを終了する
  • すべてのgoroutineが終了すると、 deadlock を起こしクラッシュする
  • panic処理と違い、defer function内のrecoverはnilを返す

なかなか使い所の難しい要素ではありますが、Goでは FailNow() などで使われたりしています。
さて、このGoexitですがそこまで使われていないっぽくて、自分自身使ってみたことがないので試してみました。

試行

それでは軽く試してみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        time.Sleep(1 * time.Second)
        runtime.Goexit()
    }()

    fmt.Printf("goroutines: %v\n", runtime.NumGoroutine())
    time.Sleep(2 * time.Second)
    fmt.Printf("goroutines: %v\n", runtime.NumGoroutine())
}

// goroutines: 2
// Done
// goroutines: 1

正常に終了し、goroutineの数が減っていることがわかりますね。
ただ、これだけでは単純に return するのと変わりません。次は少し捻ってみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        HelloAndExit()
    }()

    time.Sleep(1 * time.Second)
}

func HelloAndExit() {
    fmt.Println("Hello")
    runtime.Goexit()
}

// Hello
// Done

Hello と出力してからgoroutineを終了させるパターンです。
ただし、これだけだと

func HelloAndExit() {
    fmt.Println("Hello")
}

としても同じ結果になります。
それでは、 return との違いを試してみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done HelloAndExit goroutine")
        fmt.Println("Start HelloAndExit goroutine")
        HelloAndExit()
        HelloAndExit()
    }()

    go func() {
        defer fmt.Println("Done HelloAndReturn goroutine")
        fmt.Println("Start HelloAndReturn goroutine")
        HelloAndReturn()
        HelloAndReturn()
    }()

    time.Sleep(1 * time.Second)
}

func HelloAndExit() {
    fmt.Println("HelloAndExit")
    runtime.Goexit()
}

func HelloAndReturn() {
    fmt.Println("HelloAndReturn")
    return
}

// Start HelloAndReturn goroutine
// HelloAndReturn
// HelloAndReturn
// Done HelloAndReturn goroutine

// Start HelloAndExit goroutine
// HelloAndExit
// Done HelloAndExit goroutine

このように、 return の場合では後続処理が走り、 runtime.Goexit では実行された時点で呼び出し元goroutineが終了していることが分かりますね。
こんな風にネストを深くしても、

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        Wrapper()
    }()

    time.Sleep(2 * time.Second)
}

func Wrapper() {
    fmt.Println("Wrapper")
    HelloAndExit()
    time.Sleep(1 * time.Second)
    fmt.Println("Wake up")
}

func HelloAndExit() {
    fmt.Println("HelloAndExit")
    runtime.Goexit()
}

// Wrapper
// HelloAndExit
// Done

runtime.Goexit が呼び出された時点で、Wrapperの処理を打ち切ってgoroutineが終了されています。

まとめ

面白い機能なんですが、利用シーンを考えてみるとなかなか難しくて、「これだ!」と思うような実践的な実装が思いつきませんでした・・・
実装例を探そうとしても、そもそもGoexitを使っている人が居なかっt(ry

こんな感じで使うと良いよっていうのを、どなたかデキルGopherな方が提示してくれるのを待つしか無い・・・