contextとは?
Go言語ではcontextパッケージを利用して、WithTimeout、WithCancel、WithDeadlineを使い、main側に通知しsub goroutineを一斉キャンセルすることができます。 下記でcontextを活用する3つのパターンを紹介します。WithTimeout
タイムアウトを経過したときに一斉にsub goroutineをキャンセルする処理を書いていきます。 contextのDoneチャネルを使う事によってsub goroutinesの処理を一斉にキャンセルすることができます。package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// 400ミリ秒後にタイムアウトを設定
ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
defer cancel()
// 3つのサブゴルーチンを作成
wg.Add(3)
// idを加える("a", "b", "c")
go subTask(ctx, &wg, "a")
go subTask(ctx, &wg, "b")
go subTask(ctx, &wg, "c")
// サブゴルーチンの実行が終わるまで待機
wg.Wait()
}
func subTask(ctx context.Context, wg *sync.WaitGroup, id string) {
// サブゴルーチンの実行が終わったことを通知
defer wg.Done()
// 500ミリ秒周期でチャネルへの書き込み信号を生成する
t := time.NewTicker(500 * time.Millisecond)
select {
// contextのDoneチャネルを読み込んだときに実行する
case <-ctx.Done():
fmt.Println(ctx.Err())
return
// NewTickerへの書き込み信号を読み込んだときに実行する
case <-t.C:
// サブゴルーチンの実行が終わるまで、サブゴルーチンを実行させる信号をストップさせる
t.Stop()
fmt.Println(id)
return
}
}
実行結果は下記のようにctxのエラーメッセージが表示され,一斉にサブゴルーチンがキャンセルされる。
MacBook-puro:go-basic $ go run main.go
context deadline exceeded
context deadline exceeded
context deadline exceeded
MacBook-puro:go-basic $
以下のコードのWithTimeoutの時間を,タイムティッカーが通知される時間(500ミリ秒)よりも大きい値にすると、3つのIDが出力される。
ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
WithTimeoutの時間を600ミリ秒にする。
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
実行結果は下記のようにidが出力される。
※実行結果は順不同なので、実行する毎に出力結果は異なる。
MacBook-puro:go-basic $ go run main.go
b
c
a
MacBook-puro:go-basic $
WithCancel
下記は、サブゴルーチン毎にキャンセルとタイムアウトを通知させる処理を書いています。 normalTaskとcriticalTaskという名前のsub goroutineを作り、criticalTaskだけTimeoutによる時間制限を設定していきます。 そして、criticalTaskがタイムアウトで引っかかってしまった場合、エラー情報をmain goroutineに伝え、main goroutine側でキャンセルを実行することでnormalTaskの方も一斉にキャンセルを行っていこうと思います。package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
//deferでcancelを実行することでリソースがリークしないようにしておく
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
// criticalTaskを実行し変数に結果を代入する
v, err := criticalTask(ctx)
if err != nil {
fmt.Printf("critical task cancelled due to: %v\n", err)
//エラーが発生した場合、cancel関数を呼び出して、normalTaskをキャンセルする
cancel()
return
}
エラーが発生しなかったときは値を出力する。
fmt.Println("success", v)
}()
wg.Add(1)
go func() {
defer wg.Done()
v, err := normalTask(ctx)
if err != nil {
fmt.Printf("mormal task cancelled due to: %v\n", err)
cancel()
return
}
fmt.Println("success", v)
}()
wg.Wait()
}
func criticalTask(ctx context.Context) (string, error) {
// 800ミリ秒後にタイムアウトを設定
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
// 1000ミリ秒周期でチャネルへの書き込み信号を生成する
t := time.NewTicker(1000 * time.Millisecond)
select {
// contextのDoneチャネルを読み込んだときに実行する(空の文字列とエラーを返す)
case <-ctx.Done():
return "", ctx.Err()
// NewTickerへの書き込み信号を読み込んだときに実行する
case <-t.C:
// NewTickerの定期生成をストップさせる
t.Stop()
}
return "A", nil
}
func normalTask(ctx context.Context) (string, error) {
t := time.NewTicker(3000 * time.Millisecond)
select {
// contextのDoneチャネルを読み込んだときに実行する
case <-ctx.Done():
return "", ctx.Err()
case <-t.C:
t.Stop()
}
return "B", nil
}
上記のコードを実行すると下記のようにcriticalTaskとnormalTaskがキャンセルされます。
MacBook-puro:go-basic $ go run main.go
critical task cancelled due to: context deadline exceeded
mormal task cancelled due to: context canceled
MacBook-puro:go-basic $
criticalTaskはcriticalTaskのタイムアウトに引っかかってキャンセルされ,normalTaskはmain goroutine側の一斉キャンセルが呼ばれたことによってキャンセルされました。
以下のコードのWithTimeoutの時間を,タイムティッカーが通知される時間(1000ミリ秒)よりも大きい値にすると、criticalTask関数とnormalTask関数両方ともsuccessになる。
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
WithTimeoutの時間を1200ミリ秒にするとnormalTaskとcriticalTaskの両方が完了します。
ctx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
実行結果は下記のようになります。
MacBook-puro:go-basic sawadashinji$ go run main.go
success A
success B
MacBook-puro:go-basic sawadashinji$
WithDeadline
WithDeadlineを使用することによって時間を指定してsub goroutineをキャンセルすることができます。 今回は、Deadlineに間に合わない場合はsub goroutineをキャンセルし、Deadlineに間に合う場合は、30ミリ秒スリープしてからチャネルに「hello」を書き込む処理を実装します。package main
import (
"context"
"fmt"
"time"
)
func main() {
// Deadlineに現在時刻に20ミリ秒追加した時間をDeadlineに設定している
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Millisecond))
//deferでcancelを実行することでリソースがリークしないようにしておく
defer cancel()
ch := subTask(ctx)
v, ok := <-ch
//チャネルがクローズされてない場合、取り出した値を出力する
if ok {
fmt.Println(v)
}
fmt.Println("finish")
}
// 引数でcontextを受け取れるようにして、返り値は読み取り専用のstring型のチャネルにしている
func subTask(ctx context.Context) <-chan string {
//チャネルの作成
ch := make(chan string)
go func() {
//チャネルをクローズ
defer close(ch)
// contextに設定されているDeadlineの時刻を取得
deadline, ok := ctx.Deadline()
if ok {
if deadline.Sub(time.Now().Add(30*time.Millisecond)) < 0 {
fmt.Println("impossible to meet deadline")
return
}
}
//Deadlineに間に合う場合は、30ミリ秒スリープしてからチャネルに「hello」を書き込む
time.Sleep(30 * time.Millisecond)
ch <- "hello"
}()
return ch
}
今回の処理は、サブタスクでチャネルに「hello」を書き込む時間が30ミリ秒というのが分かっているケースを想定していて,この場合現在時刻を30ミリ秒追加した時刻がDeadlineを過ぎてしまう場合は、実行したとしても間に合わないので、goroutineを抜けてチャネルをクローズする。
if deadline.Sub(time.Now().Add(30*time.Millisecond)) < 0 {
fmt.Println("impossible to meet deadline")
return
}
上記の実行結果は以下になります。
MacBook-puro:go-basic $ go run main.go
impossible to meet deadline
finish
MacBook-puro:go-basic $
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Millisecond))
上記のコードを下記のようにWithDeadlineを40ミリ秒に設定するとチャネルにhelloが書き込まれる。
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(40*time.Millisecond))
// 実行結果
MacBook-puro:go-basic sawadashinji$ go run main.go
hello
finish