Skip to content

context

context はコンテキストと訳され、その設計の初衷は複数のコルーチン、特に親子コルーチン間で信号といくつかのシンプルなデータを伝達することです。これは HTTP リクエストの処理、タスクスケジューリング、データベースクエリなどのシーンで使用され、特にマイクロサービスアーキテクチャでは、gRPC が context を通じてプロセス間・ネットワークを越えてメタデータ伝達、リンク制御などの操作を行います。

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

上記のケースでは、context を通じてプロセス信号を伝達し、信号を受信後にプログラムは自行で終了します。これも context の 1 つの使用シーンです。

構造

context.Context は具体的な実装ではなく、一連のメソッドを定義したインターフェースです。

go
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline()、期日を返し、ブール値はすでに終了したかどうかを示します
  • Done()、終了通知に使用されるチャネルを返します
  • Err()、コンテキストが閉じられた理由を返します
  • Value()、key に基づいて指定された値を取得します

標準ライブラリには合計で以下のいくつかの使用可能なコンテキストがあります。

  • Background、空白のコンテキストで、通常はコンテキストのルートノードとして使用されます
  • WithCancelWithCancelCause、キャンセル可能なコンテキスト
  • WithDeadlineWithDeadlineCause、期日付きのコンテキスト
  • WithTimeoutWithTimeoutCause、タイムアウト時間付きのコンテキスト
  • WithValue、値を伝達できるコンテキスト

実装としては主に以下のものがあります。

  • timerCtx
  • cancelCtx
  • emptyCtx

したがって核心機能は 4 つのポイントです。

  • キャンセル
  • 期日
  • 値伝達
  • 伝播

これらを理解すれば、基本的に context の動作原理がわかります。

キャンセル

go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
	cause    error                 // set to non-nil by the first cancel call
}


func withCancel(parent Context) *cancelCtx {
    if parent == nil {
       panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

cancelCtx の核心は propagateCancel メソッドにあり、これはキャンセル可能な動作を親子コンテキストに伝播する責任を負います。

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. まず親 context がキャンセル可能かどうかをチェックします。できない場合は直接戻ります。

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. 親 context がすでにキャンセルされているかどうかをチェックします。はいの場合はすべての子 context をキャンセルします。

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. 親 context を cancelCtx タイプに変換しようとします。成功した場合は現在の context を親 context の children に追加します。

    go
    if p, ok := parentCancelCtx(parent); ok {
    	// parent is a *cancelCtx, or derives from one.
    	p.mu.Lock()
    	if p.err != nil {
    		// parent has already been canceled
    		child.cancel(false, p.err, p.cause)
    	} else {
    		if p.children == nil {
    			p.children = make(map[canceler]struct{})
    		}
    		p.children[child] = struct{}{}
    	}
    	p.mu.Unlock()
    	return
    }
  4. afterFuncer タイプに変換しようとします。成功した場合は、現在の context をキャンセルするメソッドを親 context の AfterFunc に登録します。

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent implements an AfterFunc method.
    	c.mu.Lock()
    	stop := a.AfterFunc(func() {
    		child.cancel(false, parent.Err(), Cause(parent))
    	})
    	c.Context = stopCtx{
    		Context: parent,
    		stop:    stop,
    	}
    	c.mu.Unlock()
    	return
    }
  5. それでもダメな場合は、単独でコルーチンを開始して Done チャネルを監視します。信号を受信した際に子 context をキャンセルします。

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

その後 cancelCtx.cancel メソッドが最終的に子 context をキャンセルする責任を負います。

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

そのフローは以下の通りです。

  1. 現在の context がすでにキャンセルされているかどうかをチェックします。

    go
    if err == nil {
    	panic("context: internal error: missing cancel error")
    }
    if cause == nil {
    	cause = err
    }
    c.mu.Lock()
    if c.err != nil {
    	c.mu.Unlock()
    	return // already canceled
    }
  2. done チャネルを閉じ、終了通知を送信します。

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. 循環して子 context に通知します。

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. 最後に伝われたパラメータに基づいて親 context から削除する必要があるかどうかを判断します。

    if removeFromParent {
    	removeChild(c.Context, c)
    }

期日

WithTimeoutWithDeadline はどちらも期日付きのコンテキストで、両者は同じタイプです。使用されるセマンティクスが異なるだけで、どちらも cancelCtx に基づいています。

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause は期日付きのコンテキストを作成する責任を負います。

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

そのフローは以下の通りです。

  1. 期日をチェックします。親の期日が現在の期日よりも早い場合、親 context は現在の context よりも先にキャンセルされるため、直接 cancelCtx タイプのコンテキストを作成します。

    go
    if parent == nil {
    	panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    	// The current deadline is already sooner than the new one.
    	return WithCancel(parent)
    }
  2. timerCtx を構築し、子 context に伝播します。

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. 現在の截止时间を計算します。すでに截止している場合は直接キャンセルします。

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. 截止していない場合は、time.AfterFunc を通じて截止时间に現在の context をキャンセルするように設定します。

    go
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    	c.timer = time.AfterFunc(dur, func() {
    		c.cancel(true, DeadlineExceeded, cause)
    	})
    }
    return c, func() { c.cancel(true, Canceled, nil) }

timerCtx にとって、コンテキストをキャンセルするメソッドは timer を停止し、ついでに子 context も停止するだけです。

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

値伝達

valueCtx はコンテキスト内で値を伝達・取得できます。

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

その核心は Value 関数にあり、他の context 具体的な実装もこの関数に入って値を取得します。例えば

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

value 関数は大きな for ループで、絶えず上に再帰して指定された key の値を探します。

go
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

異なる Context タイプの処理

  • *valueCtxkey が現在の Context の key にマッチする場合、key に対応する val を返します。
  • *cancelCtxkeycancelCtxKey にマッチする場合、現在の cancelCtx を返します。つまり自身を返します。
  • withoutCancelCtx:キャンセル機能のない Context を表します。keycancelCtxKey にマッチする場合、nil を返します。
  • *timerCtxkeycancelCtxKey にマッチする場合、それに関連付けられた cancelCtx を返します。
  • backgroundCtxtodoCtx:通常は追加の値を携带していない特殊なタイプの Context です。これら 2 つのタイプに遭遇した場合は直接 nil を返します。
  • 未知のタイプの場合は、Value メソッドを呼び出して探し続けます。

まとめ

これらの context の核心は cancelCtx で、これを通じてキャンセル信号を伝播します。他の context タイプおよびサードパーティの context タイプも 1 層ずつ重ねて、この基础上でさまざまな機能を実装しています。

Golang学习网由www.golangdev.cn整理维护