context 是 Go 语言应用开发常用的并发控制技术,可以用来控制多级的 goroutine,实现 goroutine 之间退出通知、元数据传递等功能。在本篇文章中,我们将从 context 的设计实现以及 context 包相关源码进行解读,帮助大家更好地理解和使用 context 包。

本文源码基于 Go 1.20

设计原理

什么是 context

context 是 Go1.7 开始标准库开始引入的,翻译为中文叫做“上下文”,准确来说它是 goroutine 的上下文,他可以控制一组呈树状结构的 goroutine,每个 goroutine 拥有相同的上下文。

context 实际上只定义了接口,接口定义如下:

// src/context/context.go
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  • Deadline() 返回完成工作的截止时间,表示上下文应该被取消的时间,和一个标识是否设置截止时间的 bool 值。如果 ok==false 则表示没有设置截至时间,此时的 deadline 为一个 time.Time 的初始值。
  • Done() 返回一个 channel,该 channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 channel;对于不支持取消的 Context,该方法可能会返回 nil,如 context.Backgroud()
  • Err() 返回 Context 结束的原因,原因由 Context 实现控制。当 context 还未关闭,Err() 返回 nil.
    • 如果 Context 被取消,则返回 context canceled 错误。
    • 如果 Context 因 deadline 关闭,则返回 context deadline exceeded 错误。
  • Value() 从 Context 中获取键对应的值。如果未设置 key 对应的值则返回 nil,以相同的 key 多次调用则返回相同的结果。

为什么有 context

context 可以用来在多个 goroutine 之间传递上下文信息,包括取消信号、超时时间、截止时间以及特定的 key-value 数据。

context-goroutine.png

在一次实际的 go 服务请求处理中,我们可能会创建多个 goroutine 来进行处理,在某些场景下,父 goroutine 退出后,和它相关联的子 goroutine 的执行都没有意义,所以子 goroutine 也需要快速的退出,此时通过 context 就可以实现这个能力。

源码解读

context 接口

如上文所示,context 是一个接口,只要实现了 context 接口中声明的所有方法,便实现了 context。

canceler 接口

canceler 接口是 context 包中用于取消操作的 context 接口,实现该接口的类型有 *cancelCtx*timerCtx

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

Done() 方法返回一个只读的 channel,可以用于检查上下文是否已被取消。当上下文被取消时,Done 方法返回的 channel 将被关闭,可以使用这个特性来通知相关的 goroutine 停止其工作并返回。

context 实现

emptyCtx

context 包中定义了一个空的 context,名为 emptyCtx,这是最简单常用的 context 类型,对 context 接口中所有方法都是空实现,主要功能是用作 context 的根节点。

type emptyCtx struct{}

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 any) any {
	return nil
}

context 包中最常用的方法是 context.Background()context.TODO(),这两个方法分别会返回私有化变量backgroundCtx{}todoCtx{},这两个变量都是指向 emptyCtx,所以本质上来说两者没有区别,只是在语义上有所不同。

// context 的默认值,一般情况下其他的 context 应该从它衍生出来
type backgroundCtx struct{ emptyCtx }

// 在不确定应该使用哪种 context 时使用
type todoCtx struct{ emptyCtx }

func Background() Context {
	return backgroundCtx{}
}

func TODO() Context {
	return todoCtx{}
}

cancelCtx

cancelCtx 是一个用于取消操作的 context,实现了 canceler 接口,并且直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    // 记录由此 context 派生的所有 context
    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
}

cancelCtx 一般由创建函数 context.WithCancel() 函数构建并暴露给用户使用,WithCancel 会返回用于取消该上下文的函数。如果执行返回的取消函数,当前上下文及其子上下文都会被取消,所有的 goroutine 都会同步收到这个取消信号。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
	return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
      // 构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
	propagateCancel(parent, c)
	return c
}

cancel() 方法的实现就是关闭 channel(c.done)来传递取消信息,并且递归地取消它的所有子结点;达到的效果是通过关闭 channel,将取消信号传递给了它的所有子结点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	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
	}
	c.err = err
	c.cause = cause
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err, cause)
	}
	c.children = nil
	c.mu.Unlock()

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

如果入参 removeFromParent 为 true,则从父结点从删除自己。

timerCtx

timerCtx 是一个可以被取消的计时器上下文,它内部通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,并且通过持有定时器 timer 和截止时间 deadline 来实现定时取消的能力。

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

	deadline time.Time
}


func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

// cancel 方法:
// 1. 调用 cancelCtx 的 cancel 方法,执行 cancel 操作;
// 2. 停止持有的定时器 timer, 减少资源浪费
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()
}

timerCtx 由创建函数 context.WithTimeout() 创建,该函数直接调用了context.WithDeadline(),传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。

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


func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	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)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, nil)
		})
	}
	return c, func() { c.cancel(true, Canceled, nil) }
}

valueCtx

valueCtx 就是在 context 的基础上加了一个键值对,用于在各级 goroutine 之间传递数据。

type valueCtx struct {
	Context
	key, val any
}

valueCtx 由创建函数 context.WithValue() 创建:

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}
}

valueCtx 不需要 cancel 和 deadline,所以只实现 Value 方法即可,如果在当前 context 中 key 值不匹配,就会从父上下文中查找该 key 对应的 value 值,直到返回 nil 或者查找到对应的值。

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

参考资料