【go】透过源码学习context

285 阅读7分钟

【go】透过源码学习context

理解

知乎上有个问题--编程中什么是「Context(上下文)」

其实我们对上下文的接触并不少,最早应该可以追溯到小学时的语文课堂,通常在学习一篇新的课文时,老师经常会就着某一个段落进行提问:结合上下文, 理解xxx(词语)代表什么含义。由此可知,上下文其实也可以说是当前的环境,包含着当前环境独有的信息,例如段落中上文的环境描写,下文主人公的行为,这在不同的文章不同的段落都是不一样的。

在编程环境中,上下文可以说是当前的运行时环境、当前的请求,包含着独有的一些环境变量,其中最典型的就是web中一个请求request的处理,不同用户的请求,用户的特征例如权限、角色、用户id,都是不一样的,而这些都可以包含在当前的Context上下文中,贯穿整个请求的链路,在需要的时候获取使用。

go源码中对context包的定义如下

context包定义了Context类型,该类型可以在跨越api边界和进程的时候携带截止期限、中止信号、以及该请求的参数。本质上是一种在 API 间树形嵌套调用时传递信号的机制。

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

接口

context.Context 是Go 语言在 1.7 版本中引入标准库的接口。

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

Deadline:  返回该context的截止期限,如果没有设置期限,则ok值为false,该接口幂等。

Done() <-chan struct{}: 返回一个channel,通过获取该channel的关闭信号来感知context的取消,返回nil代表该context永远不会被取消,多次调用返回同一个channel。

Err() error:配合Done()返回的channel使用,当channel未被关闭时,结果为nil;当channel被close(),err根据实际情况返回不同的错误。

  1. 如果是通过手动取消,则返回 Canceled 错误

var Canceled = errors.New("context canceled")

  1. 如果是超时取消,则返回 DeadlineExceed 错误

var DeadlineExceeded error = deadlineExceededError{}

当err不为nil时,多次调用都返回同样的值。

Value(key interface{}) interface{}: 存储在该context中的额外信息,通过判断key是否相等获得value;key可以是任何可以比较的值

实现

根据context.Context接口,共实现了4个具体的Ctx结构体,并且封装了6个函数供外部调用生成context实例返回。

image.png

emptyCtx

context.Todo() 和 context.Background() 返回的默认 context,是一个声明为 emptyCtx 的 int 类型,只是不同命名用于区分不同的应用场景而已。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}
func (*emptyCtx) Done() <-chan struct{} {
	return nil
}
func (*emptyCtx) Err() error {
	return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

它们的区别在于 context.Background() 一般应用于 main函数、初始化函数、测试以及作为最初的顶层context。 而 context.Todo() ,则是在不清楚当前 context 参数的用途时使用,即作为 func(ctx context.Context,.....) 中 ctx 的默认值****,而不是 使用 nil

valueCtx

context.WithValue() 返回一个包含 父contextvalueCtx实例valueCtx类型相比 emptyCtx类型 多了key、value 的属性, 用于存储额外的上下文信息。

key,value 分别是 interface{},而不是常用的 map 结构,因此 value 如果比较复杂则一般使用 struct{},同时要存储多个 key 对应的 value,则需要调用多次 context.WithValue() 方法。

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

key不能为nil,且 key 必须是可以比较的类型。

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

当通过 **Value(key interface{}) **获取 value 时,从当前 context 开始一直往父context查找,直到找到为止。


func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

一个经过几次 context.WithValue() 包装的 context 事实上是链表结构,如果并发执行多个groutine从而派生context,那就是树形。 image.png 通过context传递参数的官方实践。

package user
import (
	"context"
)
type User struct {
	Id   int64  `json:"id"`
	Name string `json:"name"`
}
type key int
// key一般不暴露
var userkey key

// 将user存储在context中
func NewContextWithUser(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userkey, u)
}
// 从context中获取到user信息
func FromContextFindUser(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userkey).(*User)
	return u, ok
}

cancelCtx

cancelCtx是分析的重点,它也是实现timerCtx的基础。

// A cancelCtx can be canceled. When canceled, it also cancels any children
// 一个可被取消的上下文,当被取消时,其下游的上下文也一并被取消
// that implement canceler.
// 它是canceler接口的实现
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields 
    // 惰性创建,被首次调用的cancel() 关闭
	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
}

当调用 context.CancelCtx() 时,内部先是调用 propagateCancel(parent, &c),最后返回一个 func() 给调用者,让调用者可以自行手动取消该上下文。

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

上面有说到,context通过一层一层的 WithXXXX 进行包装,形成了一条context链表或者树形结构,propagateCancel(parent, &c) 的作用是:

  1. 判断 父context中的cancelCtx 是否已经被取消。这里是通过一个特殊的key:cancelKey,回溯父context查找,在cacncelCtx.Value()中如果判断传递进来的key为cancelKey,则返回自身。
func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}
  1. 如果没被取消或者不存在会被取消的context,则将自身添加到父context的children字典中,构建树关系。
  2. 新开goroutine监听父context的关闭信号,当接收到信号时连同下游即children中的context级联取消。
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
        
        // 父 context 永远不会被取消,返回
		return // parent is never canceled
	}

	select {
	case <-done:
        // 父 context 已经被取消,返回
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
            // 从父context中查找到了 可取消的cancelCtx,但是已经被取消了,
            //于是级联取消下游child 
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
            // 从父context中查找到了 可取消的cancelCtx,但是还没被取消,
            // 于是添加到父context的children中
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
        // goroutines变量加1 ,用于测试,这里忽略
		atomic.AddInt32(&goroutines, +1)
        // 新开协程,监听父context的关闭信号,父context被取消时,下游context也级联取消
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

无论在哪里取消该context,最终都是调用** cancel(removeFromParent bool, err error),removeFromParent顾名思义就是是否将自身从父context的children中摘除,err则是根据当前的context类型传入 Canceled 或者 DeadlineExceeded 错误。

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's 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
	if c.done == nil {
		c.done = closedchan
	} else {
		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)
	}
}

timerCtx

context.WithTimeout() 只是 context.WithDeadline() 的封装,返回一个 timerCtx实例

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

timerCtx是在cancelCtx的基础上更进一步的组合了定时器Timer

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

	deadline time.Time
}

在 WithDeadline() 的时候除了返回 CancelFunc 给调用方手动取消context之外,还会监听timer定时器,当设定时间到达后自动执行context.cancel(),算是一种功能上的拓展吧。细节上会判断父context是否已经到达期限,做相应的处理。

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),
		deadline:  d,
	}
	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()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

一些实践

  1. Context 是 immutable 的,因此是线程安全的,可以在多个 goroutine 中传递并使用同一个 Context。
  2. Context value 是为了在请求生命周期中共享数据,而非作为函数中传递额外参数的方法。不建议在context中传递函数参数。
  3. context.TODO() 一般作为参数的默认值,而不是使用nil。
  4. empty之所以int类型而不用struct{},是因为需要返回不同的实例。
  5. 尽量不要在 struct 中存储 Context,每个函数都要显式的传递 Context,除非能把握struct的声明周期。

参考资料

  1. blog.golang.org/context
  2. github.com/golang/go/b…
  3. draveness.me/golang/docs…
  4. www.flysnow.org/2017/05/12/…
  5. zhuanlan.zhihu.com/p/163684835