タイトル: golang のコンテキスト (4)
著者: Russshare
toc: true
日付: 2021-07-13 19:19:30
タグ: [インタビュー, golang, コンテキスト]
カテゴリー: golang インタビュー
序文:
最近コルーチンについて勉強していて、同時実行性の高い複雑なネットワーク環境では、チャネルが使用要件を満たすのが難しく、コンテキストが必要になるという情報を目にしました。これも以前の事例で見たのですが、とても重要そうなので調べてみました。 2 つの記事を学びました。非常に強力なブログです。
https://studygolang.com/articles/9517
https://leileiluoluo.com/posts/golang-context.html
文章
導入
golang で新しい goroutine を作成すると、C 言語と同様に pid が返されないため、
外部から goroutine を強制終了することができません。そのため、自然に終了させる必要があります。以前は、
この問題を解決するために、channel + select を使用していました。たとえば、リクエスト
から派生したゴルーチンは、
有効期間、ルーチン ツリーの中止、リクエスト グローバル変数の転送などの機能を実装するために、特定の制約を満たす必要があります。
そこで Google は私たちに解決策を提供し、コンテキスト パッケージを公開しました。
context を使用して context 関数コントラクトを実装するには、メソッドの最初のパラメータとして
context.Context 型の変数を渡す必要があります。
1. シーン
Go サーバーでは、受信した各リクエストが独自の goroutine によって処理されることがわかっています。
たとえば、次のコードでは、リクエストごとに、ハンドラーはそれにサービスを提供するゴルーチンを作成します。また、連続する 3 つのリクエストでは、r のアドレスも異なります。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
fmt.Println(&r)
w.Write([]byte("hello"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
$ go run test.go
$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
0xc000072040
0xc000072048
0xc000072050
各リクエストに対応するハンドラーは、多くの場合、データ クエリまたは PRC 呼び出しのために追加のゴルーチンを開始します。
リクエストが返されたら、追加で作成されたこれらのゴルーチンを時間内にリサイクルする必要があります。
さらに、リクエストは、リクエスト呼び出しチェーン内の各ゴルーチンによって必要とされるリクエスト フィールド内のデータのセットに対応します。
たとえば、次のコードでは、リクエストが受信されると、
ハンドラーは監視ゴルーチンを作成し、1 秒ごとに「リクエストは処理中です」という文を出力します。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
// monitor
go func() {
for range time.Tick(time.Second) {
fmt.Println("req is processing")
}
}()
// assume req processing takes 3s
time.Sleep(3 * time.Second)
w.Write([]byte("hello"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
リクエストに 3 秒かかる、つまりリクエストが 3 秒後に戻ると仮定すると、監視ゴルーチンは「req is process」を 3 回出力した後に停止すると予想されます。
ただし、実行後、監視ゴルーチンが 3 回印刷しても終了せず、印刷を継続することがわかります。
問題は、監視ゴルーチンが作成された後、そのライフサイクルが制御されていないことです。次に、コンテキストを使用してそれを制御します。つまり、監視プログラムが印刷する前に、r.Context() が終了したかどうかを確認する必要があります。終了した場合は、ループを終了します。つまり、ライフサイクルを終了します。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
// monitor
go func() {
for range time.Tick(time.Second) {
select {
case <-r.Context().Done():
fmt.Println("req is outgoing")
return
default:
fmt.Println("req is processing")
}
}
}()
// assume req processing takes 3s
time.Sleep(3 * time.Second)
w.Write([]byte("hello"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
上記の要件に基づいて、コンテキスト パッケージ アプリケーションが誕生しました。
コンテキストパッケージは、APIリクエスト境界からリクエストドメインのデータ転送、キャンセル信号、各ゴルーチンのデッドラインまでのリクエストを提供できます。
2 コンテキストの種類
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
Done メソッドは、Context がキャンセルされるか期限に達すると閉じられるチャネルを返します。Err メソッドは、Context キャンセルの理由を返します。
コンテキスト自体には Cancel メソッドはなく、Done チャネルは信号を受信するためにのみ使用されます。キャンセル信号を受信する関数は、同時にキャンセル信号を送信する関数であってはなりません。親ゴルーチンは、一部の子操作を実行するために子ゴルーチンを開始します。子ゴルーチンを親ゴルーチンをキャンセルするために使用しないでください。
コンテキストは安全であり、複数のゴルーチンで同時に使用できます。Context は複数のゴルーチンに渡すことができ、キャンセルシグナルはこれらすべてのゴルーチンに送信できます。
期限がある場合、Deadline メソッドは Context のキャンセル時刻を返すことができます。
この値により、コンテキストがリクエスト ドメイン内でデータを運ぶことが可能になり、データ アクセスでは複数のゴルーチンによる同時アクセスのセキュリティが保証される必要があります。
派生コンテキスト
// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// A CancelFunc cancels a Context.
type CancelFunc func()
// A CancelFunc cancels a Context.
type CancelFunc func()
// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
コンテキスト パッケージは、既存のコンテキストから新しいコンテキストを派生する機能を提供します。このようにしてコンテキストツリーが形成され、
親コンテキストがキャンセルされると、そこから派生する子コンテキストもすべてキャンセルされます。
バックグラウンドはすべてのコンテキスト ツリーのルートであり、キャンセルすることはできません。
WithCancel と WithTimeout を使用すると派生コンテキストを作成でき、WithCancel を使用してそこから派生したゴルーチンのグループをキャンセルでき、WithTimeout を使用して期限を設定できます。
WithValue は、Context にフィールド データを要求する機能を提供します。
上記のメソッドの使用例をいくつか見てみましょう。
1) まず、WitchCancel の使い方を見てみましょう。
以下のコードでは、main 関数は WithCancel を使用してバックグラウンドベースの ctx を作成します。
次に、モニターのゴルーチンを開始すると、モニターは 1 秒ごとに「モニターが動作中」と出力します。
main 関数は 3 秒後にキャンセルを実行し、キャンセル信号を検出するとモニターは終了します。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// monitor
go func() {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
return
default:
fmt.Println("monitor woring")
}
}
}()
time.Sleep(3 * time.Second)
}
2) WithTimeout を使用する別の例を見てください。
次のコードは、WithTimeout を使用してバックグラウンドベースの ctx を作成します。これは 3 秒後にキャンセルされます。
なお、期限を過ぎると自動的にキャンセルされますが、キャンセルコードを追加することをお勧めします。
期限までにキャンセルされるかキャンセルコードによってキャンセルされるかは、どちらのシグナルが先に送信されたかによって異なります。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(4 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
WithDeadline の使用法は WithTimeout と似ています。
Contextの具体的な使い方を考えなければ、TODOでスペースを占有することもできますし、ツールが正しいかどうかをチェックするのにも便利です。
3) 最後に、WithValue の使用法を見てみましょう。
次のコードは、背景に基づいた値を持つ ctx を作成し、キーに従って値を取得できます。
注: コンテキストを同時に使用する複数のパッケージによって引き起こされる競合を回避するには、キーに文字列またはその他の組み込みタイプを使用するのではなく、キー タイプをカスタマイズすることをお勧めします。
package main
import (
"context"
"fmt"
)
type ctxKey string
func main() {
ctx := context.WithValue(context.Background(), ctxKey("a"), "a")
get := func(ctx context.Context, k ctxKey) {
if v, ok := ctx.Value(k).(string); ok {
fmt.Println(v)
}
}
get(ctx, ctxKey("a"))
get(ctx, ctxKey("b"))
}
最後に、コンテキストの使用規則をリストします。
a) Context を構造体のフィールドとして使用せず、それを使用する各関数のパラメーターとして使用します。一般に ctx と呼ばれる関数またはメソッドの最初のパラメーターとして定義する必要があります。
b) Context パラメータに nil を渡さないでください。その Context の使用を考えていない場合は、context.TODO を渡してください。
c) コンテキストを使用して値を渡す場合は、リクエスト フィールドのデータとしてのみ使用できます。他の種類のデータを悪用しないでください。
d) 同じコンテキストを、それを使用する複数のゴルーチンに渡すことができ、複数のゴルーチンが同時に安全にコンテキストにアクセスできます。
最後に、ソースコードを分析しながら繰り返します
context.Context インターフェイス
コンテキストパッケージのコア
// context 包里的方法是线程安全的,可以被多个 goroutine 使用
type Context interface {
// 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel
Done() <-chan struct{}
// 在 Done 的 channel被closed 后, Err 代表被关闭的原因
Err() error
// 如果存在,Deadline 返回Context将要关闭的时间
Deadline() (deadline time.Time, ok bool)
// 如果存在,Value 返回与 key 相关了的值,不存在返回 nil
Value(key interface{}) interface{}
}
このインターフェイスを手動で実装する必要はありません。コンテキスト パッケージは 2 つを提供します。1
つは Background()、もう 1 つは TODO() です。どちらの関数も Context のインスタンスを返します。
返される 2 つのインスタンスが空の Context であるというだけです。
主な構造
cancelCtx 構造体は Context を継承し、キャンセラー メソッドを実装します。
//*cancelCtx 和 *timerCtx 都实现了canceler接口,实现该接口的类型都可以被直接canceled
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // 当其被cancel时将会把err设置为非nil
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children,每个child分别cancel
//如果设置了removeFromParent,则将c从其parent的children中删除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
close(c.done)
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
}
}
timerCtx 構造体は cancelCtx から継承します
type timerCtx struct {
cancelCtx //此处的封装为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针
timer *time.Timer // Under cancelCtx.mu. 是一个计时器
deadline time.Time
}
valueCtx 構造体は cancelCtx から継承します
type valueCtx struct {
Context
key, val interface{}
}
メインメソッド
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
WithCancel は cancelCtx に対応しており、cancelCtx と
CancelFunc を同時に返します。CancelFunc
は、コンテキスト パッケージで定義されている関数タイプです (タイプ CancelFunc func())。
この CancelFunc を呼び出すときは、対応する c.done を閉じます。つまり、その子孫の goroutine を終了させます。
WithDeadline と WithTimeout は timerCtx に対応します。WithDeadline と WithTimeout は似ています。WithDeadline は
特定の期限時間を設定します。期限に達すると子孫の goroutine が終了しますが、
WithTimeout は単純で失礼で、直接 WithDeadline(parent, time.Now() を返します。 ).追加(タイムアウト))。
WithValue は valueCtx に対応し、WithValue は Context にマップを設定し、
Context を取得するゴルーチンとその子孫はマップ内の値を取得できます。
詳細なコンテキスト パッケージのソース コードの解釈: ソース コードの解釈に移動
使用原則
Context を使用するパッケージは、インターフェイスの一貫性を満たし、静的分析を容易にするために、次の原則に従う必要があります。
コンテキストを構造体に保存せず、明示的に関数に渡します。Context 変数は最初のパラメータとして使用する必要があり、通常は ctx という名前が付けられます。
メソッドで許可されている場合でも、nil Context を渡さないでください。どの Context を使用するかわからない場合は、context.TODO を渡してください。
コンテキストを使用する値関連のメソッドは、プログラムおよびインターフェイスで渡されるリクエスト関連のメタデータにのみ使用する必要があります。一部のオプションのパラメーターを渡すために使用しないでください。
同じコンテキストを使用して異なるゴルーチンに渡すことができ、コンテキストは複数のゴルーチンで安全です
使用例
例は次からコピーされています: Golang のコンテキスト パッケージの紹介
package main
import (
"fmt"
"time"
"golang.org/x/net/context"
)
// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
res := a + 1 // 虽然我只做了一次简单的 +1 的运算,
time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
// 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
return res
}
// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 如果计算被中断, 则返回 -1
func Add(ctx context.Context, a, b int) int {
res := 0
for i := 0; i < a; i++ {
res = inc(res)
select {
case <-ctx.Done():
return -1
default:
}
}
for i := 0; i < b; i++ {
res = inc(res)
select {
case <-ctx.Done():
return -1
default:
}
}
return res
}
func main() {
{
// 使用开放的 API 计算 a+b
a := 1
b := 2
timeout := 2 * time.Second
ctx, _ := context.WithTimeout(context.Background(), timeout)
res := Add(ctx, 1, 2)
fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
}
{
// 手动取消
a := 1
b := 2
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // 在调用处主动取消
}()
res := Add(ctx, 1, 2)
fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
}
}