context¶
概要¶
Context 型を提供するパッケージでデッドラインやキャンセルのシグナルやAPIの境界を超えてプロセス間でリクエストの値を渡す
Context インターフェースは4つのメソッドで構成される。コメントを除くと以下のようになる。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Contextを作るには2つのファクトリ関数が存在する。親になる基点。
func Background() Context
func TODO() Context
メソッドは以下の4つ
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
とりあえず動かしてみる¶
package main
import (
"context"
"fmt"
"time"
)
func Process(ctx context.Context, id int) {
for {
select {
// ctx.Done() 自体はチャネルを返す(かつ自身が持っているコンテキストのフィールドにチャネルをセット)
// しかし close() されていないためこの case が選択されない
case <-ctx.Done():
fmt.Printf("[goroutine %d] Process is canceled.\n", id)
return
default:
fmt.Printf("[goroutine %d] Processing...\n", id)
time.Sleep(1 * time.Second)
// something do...
}
}
}
func main() {
fmt.Printf("[goroutine main] start\n")
ctx, cancel := context.WithCancel(context.Background())
go Process(ctx, 1)
go Process(ctx, 2)
time.Sleep(1 * time.Second)
cancel()
time.Sleep(5 * time.Second)
fmt.Printf("[goroutine main] finish\n")
}
https://play.golang.org/p/l0rREisGyUQ
[goroutine main] start
[goroutine 1] Processing...
[goroutine 2] Processing...
[goroutine 1] Processing...
[goroutine 2] Process is canceled.
[goroutine 1] Process is canceled.
[goroutine main] finish
実装¶
全体的な構成(キャンセルの場合)¶
cancelCtx という構造体がコンテキストの状態を保持する重要な構造体
埋め込みされている
Context
によって親コンテキストを保持するchildren map[canceler]struct{}
が子コンテキストの一覧を保持する
// cancelCtx はキャンセル可能
// キャンセルされた場合は canceler インターフェースを実装している子どももキャンセルする
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done 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
}
// Done は c.done にチャネルを初期化(後にcloseされる)
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})
// init 時にcloseするので常にクローズしたチャネルを取得
func init() {
close(closedchan)
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
¶
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
// cancelCtx 構造体の作成(ファクトリメソッド)
c := newCancelCtx(parent)
// コンテキストの親子関係のグラフを構築
propagateCancel(parent, &c)
// 子の cancelCtx と子の cancelCtx に紐づくキャンセル関数を返却
// 親から CancelFunc を呼ぶことで cancelCtx の cancel が実行される
// この場合のみ親と子が分離される
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
// cancelCtx の Context 以外のフィールドはゼロ値で初期化
return cancelCtx{Context: parent}
}
// propagateCancel は親がキャンセルされた場合に、子をキャンセルする
func propagateCancel(parent Context, child canceler) {
// 親の場合(emptyCtx)は ctx.Done() == nil
// 親の場合はキャンセル処理をせず、アーリーリターン
if parent.Done() == nil {
return // parent is never canceled
}
// キャンセル処理の準備として map でグラフを形成
// 親コンテキストが cancelCtx 型
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
// 親コンテキストがcancelCtx型ではない場合(valueCtx型)
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
// parentCancelCtx は *cancelCtx が見つかるか emptyCtx が見つかるまでコンテキストグラフを逆向きにたどる
//
// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
//
// cancel は本質的にはチャネルのクローズ
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
// チャネルのクローズ
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// map から key を取得
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
// 葉の方向にキャンセル
child.cancel(false, err)
}
// 子の canceler はすべてキャンセル済なので map を空にする
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
// removeChild は親から(今呼ばれた)子を分離
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
// 親のコンテキストが持っている子コンテキストの map から子コンテキスト自身を削除
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
全体的な構成(デッドライン付の場合)¶
timerCtx
という構造体がcancelCtx
を内包している加えて時間に関する状態
timer *time.Timer
とdeadline time.Time
を持っている
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// Deadline() は timeCtx の deadline へのゲッター
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
// timeCtx の cancel 自体は cancelCtx で保持している cancel メソッドへのラッパーとなっている
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
// time.AfterFunc によるキャンセル処理と重複しないように、すでに設定されていれば無効化する
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
[補足] timeパッケージ¶
context
パッケージで用いられているtime
パッケージのいくつかの関数を補足しておく
func (t Time) Before(u Time) bool
¶
時刻を比較。サンプルコードが分かりやすいのでそのまま転記
year2000 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
year3000 := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)
isYear2000BeforeYear3000 := year2000.Before(year3000) // True
isYear3000BeforeYear2000 := year3000.Before(year2000) // False
fmt.Printf("year2000.Before(year3000) = %v\n", isYear2000BeforeYear3000)
fmt.Printf("year3000.Before(year2000) = %v\n", isYear3000BeforeYear2000)
func Until(t Time) Duration
¶
現在時刻から t までの期間を返却する。t.Sub(time.Now())
と同じ。現在よりも前の時刻と比較する場合は負の結果が返ってくる。
package main
import (
"fmt"
"time"
)
func main() {
// playground 上はいつも "2009-11-10 23:00:00 UTC"
year20091111 := time.Date(2009, 11, 11, 23, 0, 0, 0, time.UTC)
fmt.Printf("t.until(year20091111) = %v\n", time.Until(year20091111))
}
// t.until(year20091111) = 24h0m0s
func AfterFunc(d Duration, f func()) *Timer
¶
d
の時間経過後に f func()
を実行する
package main
import (
"fmt"
"os"
"time"
)
func main() {
time.AfterFunc(3 * time.Second, func() {
fmt.Println("Timeout")
os.Exit(0)
})
select {}
}
// Timeout ※ 3秒経過後に
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
¶
WithCancel
に d
を組み合わせたラッパー
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
// input の期限を保持
deadline: d,
// timer *time.Timer は time.AfterFunc でセットされる
}
// グラフの構築
propagateCancel(parent, c)
// すでに期限到来の場合はキャンセルする
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
// time.AfterFunc を使用して dur 時間経過後にキャンセルを実行
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
¶
こちらも WithDeadline
のラッパー。期限を指定された場合は、現在時刻に期限を追加した時刻として WithDeadline
を呼び出すようになっている
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithValueの場合¶
キャンセル処理ではなく、値を引き回すためのコンテキスト
元のコンテキストに key, value をラップしたコンテキストを返すだけ
どんな感じで使われているか?¶
構造体の key, value を持つコンテキストということが実装からもわかる
https://github.com/google/gapid/blob/master/core/data/id/id_remap.go#L25-L48
// Remapper is an interface which allows remapping between ID to int64.
// One such remapper can be stored in the current Context.
// This is used to handle resource when converting to/from proto.
// It needs to live here to break go package dependency cycles.
type Remapper interface {
RemapIndex(ctx context.Context, index int64) (ID, error)
RemapID(ctx context.Context, id ID) (int64, error)
}
type remapperKeyTy string
const remapperKey = remapperKeyTy("remapper")
// GetRemapper returns the Remapper attached to the given context.
func GetRemapper(ctx context.Context) Remapper {
if val := ctx.Value(remapperKey); val != nil {
return val.(Remapper)
}
panic("remapper missing from context")
}
// PutRemapper amends a Context by attaching a Remapper reference to it.
func PutRemapper(ctx context.Context, d Remapper) context.Context {
if val := ctx.Value(remapperKey); val != nil {
panic("Context already holds remapper")
}
return context.WithValue(ctx, remapperKey, d)
}
type valueCtx struct {
Context
// (構造体の) key, val をフィールドとして保持
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
// 比較可能な型かどうかは以下を参照
// https://golang.org/ref/spec#Comparison_operators
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}