2
1

Go言語のContextについて

Last updated at Posted at 2024-07-16

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

最後に

今回、Go言語のContextについてどうやって使い分けるのか理解できていないと感じたので、自分の学習した内容をまとめてみました。 WithTimeout、WithCancel、Deadlineの違いや使い方がまとめる以前より理解が深まったと思ってます。
2
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
2
1