LoginSignup
1
1

More than 3 years have passed since last update.

Go で 無限ループをタイムアウトもしくは、Ctr+C で終了させたい

Posted at

Go で、非同期リクエストを実施した後に、Pollingの処理を書いている。ふと気づいたのだが、その処理は無限ループの処理で、タイムアウトを設定している。しかし、タイムアウトまで長いので処理を中断させたいケースはままあるのだが、そういう時はどうするのだろう?

シグナルを受信する

最初に必要なのは、Ctr+cのシグナルを受信することだ。signal.Notify の関数が使える。

func Notify(c chan<- os.Signal, sig ...os.Signal)

os.Signal のチャネルを作って、受信したいシグナルを列挙すればよさげ。ちなみに、シグナルを指定しないと、すべてのシグナルを受信するのがデフォルトになります。

シグナルの種類

受信できるシグナルの種類ですが、Go の場合こちらに書いてあります。

Linuxのシグナルと同じになっていますが、そもそもLinux のシグナルにどんなものがあったのか覚えていません。

上記のマニュアルを見ると、SIGINT が該当しそうです。コマンドラインツールなので、SIGHUPも受信したほうがいいかなぁ、、、と思ったけど(例えば無限ループしている状態で、Terminal落としたとか)、その場合は、コマンドのプロセスごと死ぬからまあいいかと。

シグナル 値 動作 コメント
SIGHUP 1 Term 制御端末(controlling terminal)のハングアップ検出、
または制御しているプロセスの死
SIGINT 2 Term キーボードからの割り込み (Interrupt)
SIGQUIT 3 Core キーボードによる中止 (Quit)
SIGILL 4 Core 不正な命令
SIGABRT 6 Core abort(3) からの中断 (Abort) シグナル
SIGFPE 8 Core 浮動小数点例外
SIGKILL 9 Term Kill シグナル
SIGSEGV 11 Core 不正なメモリー参照
SIGPIPE 13 Term パイプ破壊:
読み手の無いパイプへの書き出し
SIGALRM 14 Term alarm(2) からのタイマーシグナル
SIGTERM 15 Term 終了 (termination) シグナル
SIGUSR1 30,10,16 Term ユーザー定義シグナル 1
SIGUSR2 31,12,17 Term ユーザー定義シグナル 2
SIGCHLD 20,17,18 Ign 子プロセスの一時停止 (stop) または終了
SIGCONT 19,18,25 Cont 一時停止 (stop) からの再開
SIGSTOP 17,19,23 Stop プロセスの一時停止 (stop)
SIGTSTP 18,20,24 Stop 端末より入力された一時停止 (stop)
SIGTTIN 21,21,26 Stop バックグランドプロセスの端末入力
SIGTTOU 22,22,27 Stop バックグランドプロセスの端末出力

SIGINT 受信で終了する際の終了ステータスコード

SIGINTを受け付けて終了する場合のステータスコードって何になるのでしょう?下記のブログにとてもいい表がありました。

上記ブログより

終了コード 意味 コメント
1 一般的なエラーの catch-all let "var1 = 1/0" "divide by zero" や、他の許容されない操作を含む各種のエラー
2 シェル内蔵機能の誤使用 (Bash ドキュメントに従う) empty_function() {} キーワードが見つからないまたはコマンドが存在しない、またはアクセス許可の問題 (および、バイナリファイル比較の失敗時に diff が返すリターンコード)
126 起動されたコマンドが実行できない /dev/null アクセス許可の問題、またはコマンドが実行形式ファイルでない
127 "command not found" illegal_command $PATH の問題、またはタイプミス
128 exit への無効な引数 exit 3.14159 exit は 0 ~ 255 の範囲の整数値のみを引数として受け付けます (最初の脚注を参照)
128+n 致命的なエラー信号 "n" kill -9スクリプトの $PPID $?137 (128 + 9) を返す
130 スクリプトが Control-C により終了された Ctl-C Control-C は致命的なエラー信号 2 (130 = 128 + 2、上記参照)
255* 終了ステータスの範囲外 exit -1 exit は 0 ~ 255 の範囲の整数値のみを引数として受け付けます

これでいくと今回は 130 でExit したらよさそうですが、まずい、今まで問題があったら 1 で終了させていた。(CIで使うことを想定したツールだったので、終了させたかった)でも本当は終了ステータスはいろいろだから、ちょっとこの辺はライブラリ書いたほうがよさそう(もしくは、ライブラリを探すか、、、)

サンプルコード

サンプルコードは次の感じです。

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    fmt.Println("step1")
    <-quit
    fmt.Println("step2")

こちらを実行すると、step1 のみが最初に表示されて、ctr+cを受信したら step2が表示されて終了します。Notifyは、処理をブロックしませんのので、こんな感じで書けそうです。

タイムアウトと、複数のチャネルを受信する

シグナルの受信の方法はわかったのですが次の課題は、複数のチャネルを扱う方法です。シグナルを受信したら、チャネル経由で受け取るわけですが、Pollingの無限ループは現在も、タイムアウトの処理が必要で、尚且つ、現在は無限ループを go routine でラップしていて、チャネルを受け取っているので、複数のチャネルの処理が必要です。

結論からいくと、go lang の select は相当強力で、複数のチャネルの受信も書くことができます。

func main() {
    timeout := 30
    quit := make(chan os.Signal)
    c := make(chan string, 1)
    signal.Notify(quit, os.Interrupt)
    go func() {
        result := loop()
        c <- result
    }()
    select {
    case result := <-c:
        fmt.Println(result)
    case <-quit:
        fmt.Println("Interrupt signal accepted.")
    case <-time.After(time.Duration(30) * time.Second):
        fmt.Printf("Timeout happened. exceed %d second.\n", timeout)
    }
}

func loop() string {
    counter := 0
    for {
        time.Sleep(5 * time.Second)
        counter++
        fmt.Println("Do my job:" + strconv.Itoa(counter))
        if counter == 50 {
            return "done"
        }
    }
}

実行例

$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Do my job:4
Do my job:5
Timeout happened. exceed 30 second.

ctr+c

$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Interrupt signal accepted.

タイムアウト処理

タイムアウト処理を解説するとtime.After関数を使っています。一定期間が過ぎると、チャネルに送信します。ちなみに、このマニュアルを読んでいると、効率のためには、NewTimer を使えとあります。

func After(d Duration) <-chan Time

折角なのでこちらも試してみましょう。戻りがTimerなので、どうやってチャネルを待つのだろうと思ったのですが、Timerのストラクトに、Cというチャネルを持っているのでそれを使えば良さげです。

func NewTimer(d Duration) *Timer

先ほどのプログラムを一部変更

    // case <-time.After(time.Duration(30) * time.Second):
    case <-time.NewTimer(time.Duration(30) * time.Second).C:
        fmt.Printf("Timeout happened. exceed %d second.\n", timeout)

実行結果

同じように動作しているようです。

$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Do my job:4
Do my job:5
Timeout happened. exceed 30 second.

リソース

ちなみに今回の調査で、go routine のパターンのブログを見つけたので貼っておきます。

go routine と select はもうちょい勉強せなあかんなぁ。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1