time.Ticker で遊ぶ
相変わらず小ネタで。 今回の目標は2つ。
- 一定周期ごとの処理を行う
- Ctrl+C 等の割り込み処理を行う
一定周期ごとの処理を行う
Go 言語で一定周期ごとに処理を行うには time
.Ticker
が使える。
以下は1秒ごとに現在時刻を表示する処理である。
package main
import (
"fmt"
"time"
)
func ticker() {
t := time.NewTicker(1 * time.Second) //1秒周期の ticker
defer t.Stop()
for {
select {
case now := <-t.C:
fmt.Println(now.Format(time.RFC3339))
}
}
}
func main() {
ticker()
}
time
.Ticker.C
は受信 channel で,周期イベント発生時の時刻がセットされる。
defer 構文を使って終了時に time
.Ticker.Stop()
関数で周期イベントを止めようとしているが,実際には無限ループなので, return まで到達しない(笑)
このコードはちゃんと動くが,終了条件を記述していないので Ctrl+C などで外部から強制的に止めない限り動き続ける。
SIGNAL を捕まえる
Go 言語では SIGINT や SIGTERM といった OS から送信される SIGNAL をイベントとして channel に送り込む仕掛けがある(ちなみに Ctrl+C は SIGINT として送られる)。 この仕掛けを組み込んだコードが以下である。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func ticker() {
t := time.NewTicker(1 * time.Second) //1秒周期の ticker
defer t.Stop()
sig := make(chan os.Signal, 1)
signal.Notify(sig,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
defer signal.Stop(sig)
for {
select {
case now := <-t.C:
fmt.Println(now.Format(time.RFC3339))
case s := <-sig:
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Println("Stop!")
return
}
}
}
}
func main() {
ticker()
}
このやり方であれば Ctrl+C でも強制終了することなく正しく time
.Ticker.Stop()
関数が起動される。
ここまでが main goroutine のみの処理の場合。 複数の goroutine が協調して動いている場合は SIGNAL イベントに対して全ての goroutine が適切に処理を行う必要がある。
キャンセル・イベントの伝搬
Go 言語 1.7 から context
が標準パッケージに加わった。
context
パッケージは,名前の通り,異なるレイヤやドメイン間でコンテキスト情報を受け渡しするためのパッケージだが,キャンセル・イベントを扱うことができる。
ctx := context.Background()
parent, cancelParent := context.WithCancel(ctx)
child, cancelChild := context.WithCancel(parent)
cancelParent
および cancelChild
は関数値で,これをキックすることでそれぞれの context
.Context
にキャンセル・イベントが発生する。
面白いのは parent
で発生したイベントは child
にも伝搬する点である(逆向きには伝搬しない)。
これを利用して,発生した SIGNAL に対して親の context
.Context
にイベントを発生させることによって全ての子 context
.Context
にイベントを伝搬させることが可能になる。
たとえば,先ほどの SIGNAL の処理は以下のように書き直すことができる。
func SignalContext(ctx context.Context) context.Context {
parent, cancelParent := context.WithCancel(ctx)
go func() {
defer cancelParent()
sig := make(chan os.Signal, 1)
signal.Notify(sig,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
defer signal.Stop(sig)
select {
case <-parent.Done():
fmt.Println("Cancel from parent")
return
case s := <-sig:
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Println("Stop!")
return
}
}
}()
return parent
}
ticker()
関数のほうも context
.Context
のイベントを拾えるよう以下のように書き直す。
func ticker(ctx context.Context) error {
t := time.NewTicker(1 * time.Second) //1秒周期の ticker
defer t.Stop()
for {
select {
case now := <-t.C:
fmt.Println(now.Format(time.RFC3339))
case <-ctx.Done():
fmt.Println("Stop child")
return ctx.Err()
}
}
}
周期処理と SIGNAL 受信処理を別々の goroutine で駆動させて両者を context
.Context
で繋ぐのである。
完全なコードは以下の通り。
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func ticker(ctx context.Context) error {
t := time.NewTicker(1 * time.Second) //1秒周期の ticker
defer t.Stop()
for {
select {
case now := <-t.C:
fmt.Println(now.Format(time.RFC3339))
case <-ctx.Done():
fmt.Println("Stop child")
return ctx.Err()
}
}
}
func SignalContext(ctx context.Context) context.Context {
parent, cancelParent := context.WithCancel(ctx)
go func() {
defer cancelParent()
sig := make(chan os.Signal, 1)
signal.Notify(sig,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
defer signal.Stop(sig)
select {
case <-parent.Done():
fmt.Println("Cancel from parent")
return
case s := <-sig:
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Println("Stop!")
return
}
}
}()
return parent
}
func Run(ctx context.Context) error {
errCh := make(chan error, 1)
defer close(errCh)
parent := SignalContext(ctx)
go func() {
child, cancelChild := context.WithTimeout(parent, 10*time.Second)
defer cancelChild()
errCh <- ticker(child)
}()
err := <-errCh
fmt.Println("Done parent")
return err
}
func main() {
ctx := context.Background()
if err := Run(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
fmt.Println("Done")
}
ちなみに context
.WithTimeout()
関数は context
.Context
にタイムアウト・イベントを付加する。
他にもデッドライン日時を指定する context
.WithDeadline()
関数がある。
なんか今回は久しぶりに Go 言語っぽいコードだったねぇ(笑)
【追記】 Windows では SIGNAL を送信できない
今回は SIGNAL を受信する場合の話だったが,もちろん任意のプロセスに SIGNAL を送信することもできる。 以下は自分自身に SIGINT を投げるコード例である1。
proc, _ := os.FindProcess(os.Getppid())
proc.Signal(os.Interrupt)
ただし,このコードは Windows では(コンパイル・エラーにはならないが)動かない。
Windows 用の os
.Process.Signal()
関数の実体は以下の通りだが
func (p *Process) signal(sig Signal) error {
handle := atomic.LoadUintptr(&p.handle)
if handle == uintptr(syscall.InvalidHandle) {
return syscall.EINVAL
}
if p.done() {
return errors.New("os: process already finished")
}
if sig == Kill {
err := terminateProcess(p.Pid, 1)
runtime.KeepAlive(p)
return err
}
// TODO(rsc): Handle Interrupt too?
return syscall.Errno(syscall.EWINDOWS)
}
このように os
.Kill
(SIGKILL) 以外は効かないようになっている。
理由は不明だが TODO になってるようなので何か理由があるのだろう。
というわけでテストができないのですよ orz
ブックマーク
- Go Concurrency Patterns: Pipelines and cancellation - The Go Blog
- Go1.7のcontextパッケージ | SOTA
- Golangのcontext.Valueの使い方 | SOTA
- goroutine にシグナルを送信する - Qiita
- signal.Notifyを使うときは必ずバッファ付きチャネルで利用すること - My External Storage
- Big Sky :: os/signal に NotifyContext が入った。
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。