2019年2月5日火曜日

Context + os/signal でバックグランド処理を強制的に終了させる

概要

例えば処理が長い関数がある場合にその処理を Ctrl+c などで終了したいケースはあると思います
そんな場合に Context と Signal を使えば実現することができます

環境

  • macOS 10.14.2
  • golang 1.11.2

サンプルコード

少し長めです
分かりやすいようにコメントを入れています
ポイントはシグナルを go routine 内の select 文でハンドリングする点とその際に Context を cancel() することで関数を終了させる点かなと思います

詳細は以下サンプルコードとコメントを見てもらえればと思います

package main

import (
    "context"
    "errors"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// 長くなるであろう任意の処理、強制終了された場合はエラーを返す
func robot(ctx context.Context) error {
    count := 1
    for {
        if count == 3 {
            // 3 回ループしたら正常終了
            fmt.Println("robot end")
            return nil
        }
        select {
        case <-ctx.Done():
            // case <-c: の cancel がコールされたタイミングでここがコールされる
            fmt.Println("force robot end")
            return errors.New("error")
        default:
            fmt.Println(count)
            time.Sleep(1 * time.Second)
            count++
        }
    }
}

// case <-ctx.Done(): が実行されているか確認するためのデバッグメソッド
func debug() {
    fmt.Println("debug")
}

// メイン
func main() {
    // バックグランドコンテキスト作成
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    // 受信したシグナルを管理するためのチャネル
    c := make(chan os.Signal, 1)
    // ハンドリングするシグナルの種類を登録する、os.Interrupt は Windows 用の Ctrl+c
    signal.Notify(c,
        syscall.SIGINT,
        os.Interrupt)
    // main() 終了時に必ずコールされる
    defer func() {
        // シグナルの受付を終了する
        signal.Stop(c)
        fmt.Println("defer")
    }()
    // ここの goroutine でシグナルのハンドリングとコンテキストの終了を待機する
    go func() {
        select {
        case <-c:
            // シグナルをキャッチ
            fmt.Println("signal")
            // robot() を終了させる
            cancel()
        case <-ctx.Done():
            // cancel() コール時に呼ばれる、ただしシグナルをキャッチした際の cancel() ではここは通らない
            debug()
            fmt.Println("ctx.Done()")
        }
    }()
    ret := robot(ctx)
    // エラーじゃない場合は context 終了させる
    if ret == nil {
        cancel()
    }
}

参考サイト

0 件のコメント:

コメントを投稿