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
错误。
- 如果 Context 被取消,则返回
Value()
从 Context 中获取键对应的值。如果未设置 key 对应的值则返回 nil,以相同的 key 多次调用则返回相同的结果。
为什么有 context
context 可以用来在多个 goroutine 之间传递上下文信息,包括取消信号、超时时间、截止时间以及特定的 key-value 数据。
在一次实际的 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)
}