sgykfjsm.github.com

GolangのSignalハンドリングを同時に複数実行したらどうなるか

めっちゃ久しぶりにブログを書く。標題の件が気になったので実際にやってみた。

シンプルなSignalハンドラはこんな感じになる。ほぼGo By Exampleと同じ。何度も写経しているうちに暗記した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"
)

func main() {
  sig := make(chan os.Signal, 1)
  done := make(chan bool, 1)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  go func() {
      s := <-sig
      fmt.Printf("\nCatch the signal at main: %v\n", s)
      done <- true
  }()

  fmt.Println("Waiting for signal at main ...")
  <-done
  fmt.Println("Exit from main")
}

次にほぼこれをコピペしてライブラリとしてインポートする。そうすると、以下のようになる。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"

  "github.com/sgykfjsm/sample-program-by-go/signal-handler/sub1"
  "github.com/sgykfjsm/sample-program-by-go/signal-handler/sub2"
  "sync"
)

func main() {
  sig := make(chan os.Signal, 1)
  done := make(chan bool, 1)

  var wg sync.WaitGroup

  go sub1.Sub1(&wg)
  go sub2.Sub2(&wg)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  wg.Add(1)
  go func() {
      defer wg.Done()
      s := <-sig
      fmt.Printf("\nCatch the signal at main: %v\n", s)
      done <- true
  }()

  fmt.Println("Waiting for signal at main ...")
  wg.Wait()
  <-done
  fmt.Println("Exit from main")
}

sub1/sub.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package sub1

import (
  "fmt"
  "os"
  "os/signal"
  "sync"
  "syscall"
)

func Sub1(wg *sync.WaitGroup) {
  // Don't put wg.Wait(). This should be done at caller function.
  wg.Add(1)
  sig := make(chan os.Signal, 1)
  done := make(chan bool, 1)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  go func() {
      defer wg.Done()
      s := <-sig
      fmt.Printf("\nCatch the signal at Sub1: %v\n", s)
      done <- true
  }()

  fmt.Println("Waiting for signal at Sub1 ...")
  <-done
  fmt.Println("Exit from Sub1")
}

sub2/sub.goも作っているけど、sub1/sub.goとほぼ同じなので省略。先にmain.goがexitしないようにsync.Waitgroupを使って待ち合わせをする。実行結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub2 ...
Waiting for signal at Sub1 ...
^C
Catch the signal at Sub1: interrupt
Exit from Sub1

Catch the signal at main: interrupt

Catch the signal at Sub2: interrupt
Exit from main

$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub1 ...
Waiting for signal at Sub2 ...
^C
Catch the signal at main: interrupt

Catch the signal at Sub1: interrupt
Exit from Sub1

Catch the signal at Sub2: interrupt
Exit from main

$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub2 ...
Waiting for signal at Sub1 ...
^C
Catch the signal at main: interrupt

Catch the signal at Sub2: interrupt
Exit from Sub2

Catch the signal at Sub1: interrupt
Exit from main

見ての通り、各ハンドラは同一のシグナルを同様に受け取っていることがわかる。まぁそりゃそうだという感じ。実際にコード(というかgodoc)にも以下の様に記述されている

It is allowed to call Notify multiple times with different channels and the same signals: each channel receives copies of incoming signals independently.

ただし、上記出力からもう1つわかることとして、ライブラリ側のwg.Done()以降の処理は必ずしも全うするとは限らないということだ。そのため、例えば、シグナルを受け取ったらメモリに溜まっているデータをFlushして何らかの処理などを行う必要がある場合、呼び出し側の処理を止めてライブラリ側の処理を終了させてから次に呼び出し側の処理を再開するような実装をしなければならない。

今回のサンプルの場合であれば、wg.Doneを処理の一番最後に持っていけば良い。例えば、以下の様にする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Sub2(wg *sync.WaitGroup) {
  defer wg.Done() // <-- 処理の最初にdeferでwg.Doneを登録する。
  // Don't put wg.Wait(). This should be done at caller function.
  wg.Add(1)
  sig := make(chan os.Signal, 1)
  done := make(chan bool, 1)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  go func() {
      s := <-sig
      fmt.Printf("\nCatch the signal at Sub2: %v\n", s)
      done <- true
  }()

  fmt.Println("Waiting for signal at Sub2 ...")
  <-done
  fmt.Println("Exit from Sub2")
}

しかし、現実として呼び出し側からwg *sync.WaitGroupを渡してもらうということは実装として通常はありえない。ではどうするか?

1つの方法としてはruntime.SetFinalizerを使う方法がある。これはGCが発動する際にGC処理のfinalizerとして処理を登録する方法だ。参考の実装は https://gist.github.com/deltamobile/6511901 がある。https://play.golang.org/p/jWhRSPNvxJ で動作を確認することができる。一見、これは有用に見えるが、GC処理のfinalizerとするのはあまり実用的ではない。

自分がおもいつく限りだとsync.Mutexを使った排他制御かなーと思ったけど、どういう風に実装にすれば良いか思いつかない。やっぱりシンプルにチャンネルを渡して、呼び出し側が渡したチャンネルから終了の合図を受け取る方法かなぁ?

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"

  "github.com/sgykfjsm/sample-program-by-go/signal-handler/sub1"
  "github.com/sgykfjsm/sample-program-by-go/signal-handler/sub2"
)

func main() {
  sig := make(chan os.Signal, 1)
  done := make(chan bool, 1)
  sub1Done := make(chan bool, 1)
  sub2Done := make(chan bool, 1)

  go sub1.Sub1(sub1Done)
  go sub2.Sub2(sub2Done)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  go func() {
      s := <-sig
      fmt.Printf("\nCatch the signal at main: %v\n", s)
      done <- true
  }()

  fmt.Println("Waiting for signal at main ...")
  <-done
  fmt.Println("Exit from main")

  <-sub1Done
  <-sub2Done
}

sub1/sub.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package sub1

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"
)

func Sub1(done chan<- bool) {
  sig := make(chan os.Signal, 1)
  subDone := make(chan bool, 1)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  go func() {
      s := <-sig
      fmt.Printf("\nCatch the signal at Sub1: %v\n", s)
      subDone <- true
  }()

  fmt.Println("Waiting for signal at Sub1 ...")
  <-subDone
  fmt.Println("Exit from Sub1")

  done <- true
}

結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub2 ...
Waiting for signal at Sub1 ...
^C
Catch the signal at main: interrupt
Exit from main

Catch the signal at Sub2: interrupt
Exit from Sub2

Catch the signal at Sub1: interrupt
Exit from Sub1

$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub2 ...
Waiting for signal at Sub1 ...
^C
Catch the signal at Sub2: interrupt
Exit from Sub2

Catch the signal at main: interrupt
Exit from main

Catch the signal at Sub1: interrupt
Exit from Sub1

$ go run ./main.go
Waiting for signal at main ...
Waiting for signal at Sub1 ...
Waiting for signal at Sub2 ...
^C
Catch the signal at Sub2: interrupt
Exit from Sub2

Catch the signal at main: interrupt
Exit from main

Catch the signal at Sub1: interrupt
Exit from Sub1

一応は期待通りに、ライブラリ側の処理を全うして呼び出し側が終了させることが出来た。最初の例と何が違うのかって言われるとちょっと困るけど、チャンネルを通じたコミュニケーションということで、こっちのほうがGoっぽいかな?という程度…どこかに最適解がありそうな気がするけど、まぁ今日はこのへんで。