1. ホーム
  2. スクリプト・コラム
  3. ゴラン

Goにおけるタイムアウト制御の解決策

2022-02-14 01:57:44

前文

日々の開発の中で、時間のかかるタスクの一括処理、ネットワークリクエストなど、タイムアウト制御のシナリオに遭遇することがあると思います。優れたタイムアウト制御は、いくつかの問題を効果的に回避することができます (例. goroutine リーク、リソースの非放出など)。

タイマー

goでタイムアウト制御を実装する方法は非常にシンプルで、まず最初のオプションである Time.After(d Duration) :

func main() {
	fmt.Println(time.Now())
	x := <-time.After(3 * time.Second)
	fmt.Println(x)
}

を出力します。

2021-10-27 23:06:04.304596 +0800 CST m=+0.000085653
2021-10-27 23:06:07.306311 +0800 CST m=+3.001711390

time.After() が返されます。 Channel その Channel は、d時間後にデータを書き込む。

この機能により、いくつかの非同期制御のタイムアウトシナリオを実装することが可能である。

func main() {
	ch := make(chan struct{}, 1)
	go func() {
		fmt.Println("do something... ")
		time.Sleep(4*time.Second)
		ch<- struct{}{}
	}()
	
	select {
	case <-ch:
		fmt.Println("done")
	case <-time.After(3*time.Second):
		fmt.Println("timeout")
	}
}

ここでは goroutine は、select を使用する時間のかかるタスクを実行することで channel データを取得した時点で終了するもの。 goroutine が限られた時間内にタスクを完了しない場合、メインの goroutine で、タイムアウトの役割も果たす。

を出力します。

何とかしてくれ
タイムアウト

キャンセル後、チャンネルがメッセージを送信する間、またはチャンネルや他の通知方法を閉じることができます。

チャネルは、ゴルーチンをブロックしてリークを起こさないようなサイズにするのがベストであることに注意してください。

コンテキスト

2つ目の選択肢はコンテキストを使うことで、goのコンテキストは強力です。


を使用する context.WithTimeout() メソッドを使用すると、タイムアウト機能を持つコンテキストが返されます。

ch := make(chan string)
	timeout, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	go func() {
		time.Sleep(time.Second * 4)

		ch <- "done"
	}()

	select {
	case res := <-ch:
		fmt.Println(res)
	case <-timeout.Done():
		fmt.Println("timout", timeout.Err())
	}

同じ使い方で context Done() 関数は channel その channel は、現在のジョブが完了したとき、あるいはコンテキストがキャンセルされたときに有効になります。

timout コンテキストの期限を超えました

By timeout.Err() また、現在の context が閉じている。

goroutine コンテキストの受け渡し

を使用します。 context には、複数のゴルーチンに渡って渡される自然な性質を利用して、コンテキストを渡した全てのゴルーチンが同時にキャンセル通知を受け取れるという利点があり、これは複数のゴで非常に広く使われています。

func main() {
	total := 12
	var num int32
	log.Println("begin")
	ctx, cancelFunc := context.WithTimeout(context.Background(), 3*time.Second)
	for i := 0; i < total; i++ {
		go func() {
			//time.Sleep(3 * time.Second)
			atomic.AddInt32(&num, 1)
			if atomic.LoadInt32(&num) == 10 {
				cancelFunc()
			}
		}()
	}
	for i := 0; i < 5; i++ {
		go func() {

			select {
			case <-ctx.Done():
				log.Println("ctx1 done", ctx.Err())
			}

			for i := 0; i < 2; i++ {
				go func() {
					select {
					case <-ctx.Done():
						log.Println("ctx2 done", ctx.Err())
					}
				}()
			}

		}()
	}

	time.Sleep(time.Second*5)
	log.Println("end", ctx.Err())
	fmt.Printf("execution finished %v", num)
}

上記の例では、どのような goroutine が何重にもネストされている場合は context メッセージがキャンセルされたとき(もちろん、このとき context を渡さなければならない)

を手動で呼び出すこともできます。 cancelFunc() 関数を使用します。

Ginの場合

Ginでは Shutdown(ctx) 関数も駆使して context .

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err ! = nil {
		log.Fatal("Server Shutdown:", err)
	}
	log.Println("Server exiting")

例えば、上記のコードは、タイムアウトで10秒待つと Gin これは、上記の例と同じ原理を実装したものです。

概要

久しぶりに碁を書いたので、練習がてら自分のプロジェクトを書きました:インターフェースストレステストツールです。


非常に一般的な要件として、圧力テストのN秒後に終了することが挙げられますが、ここでまた初めて、知識が応用されることになります go というところからスタートします。

https://github.com/crossoverJie/ptg/blob/d0781fcb5551281cf6d90a86b70130149e1525a6/duration.go

以上でGoのタイムアウト制御についての記事を終わります。Goのタイムアウト制御については、Script Houseの過去記事を検索するか、引き続き以下の関連記事を参照してください。