Go源码学习:Context

223 阅读6分钟

本文涉及源码基于Go1.17.3版本

认识Context

type Context interface {
   Deadline() (deadline time.Time, ok bool)

   Done() <-chan struct{}

   Err() error

   Value(key interface{}) interface{}
}

Context实际上只是一个接口,任何实现了该接口下的四个方法都可以被看作是Context:

  • Deadline:只有当context能够被cancel时,该方法有效,返回cancel时刻的time和true,否则返回默认time和false。
  • Done:该方法返回一个仅用于观察该context是否被cancel的通道,如果该context被设定为无法cancel,则应返回nil
  • Err:如果该context被cancel,则返回cancel的原因,否则返回nil
  • Value:用于在context之间传递信息,根据key值读写value

以上四个方法在一定条件下皆为幂等。

Context的主要作用:

  • 在并发过程中,能够通过设定超时时间或取消信号来控制携带context的goroutine的生命周期
  • 通过Value传递上下文信息

Context预设类型:

emptyCtx:

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
}

可以看到系统的emptyCtx实际上是int类型,实现的方法都是返回默认值or nil,即非canceled的ctx。

Background() & TODO()

emptyCtx主要是root Context的类型,平常使用的context.Background()获取到的ctx就是emptyCtx:

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

func Background() Context {
   return background
}

func TODO() Context {
   return todo
}

从源码可以看到,context.Background()返回的是一个全局变量background(emptyCtx的指针)

我们可以打印下看看:

ctx := context.Background()
fmt.Printf("ctx: %+v", ctx)

预期应该打印出来的是一个类似0xc233333333的地址,但实际上输出的是:

ctx: context.Background

原因:emptyCtx实现了String()方法

func (e *emptyCtx) String() string {
   switch e {
   case background:
      return "context.Background"
   case todo:
      return "context.TODO"
   }
   return "unknown empty Context"
}

这里实际上隐藏了background为一个emptyCtx指针的事实。

  • 我们通常在主函数or首次请求or测试中初始化并获取backgroundCtx,并传递给下游
  • 而todoCtx则是在不确定使用哪种context时,或是当前下游函数还未扩展到接受context参数时的临时写法。

valueCtx:

type valueCtx struct {
   Context
   key, val interface{}
}

与emptyCtx的int类型不同,valueCtx是一个自定义的结构体,除了实现Context的四种方法外,还保存着key和value。

WithValue()

先看一下valueCtx的初始化:

func WithValue(parent Context, key, val interface{}) 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}
}

首先入参必须要传一个父ctx,其次key值不能为nil,key的类型必须Comparable,否则直接panic。

Value()

接着来看下valueCtx实现的Value()方法:

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

如果传入的key为当前valueCtx的key,则直接返当前valueCtx的value,否则去该valueCtx的父ctx去找key值对应的value,也就是说可以看作ctx的value可以被继承的:

image.png

String()

然后是剩下两个打印函数:

func stringify(v interface{}) string {
   switch s := v.(type) {
   case stringer:
      return s.String()
   case string:
      return s
   }
   return "<not Stringer>"
}

func (c *valueCtx) String() string {
   return contextName(c.Context) + ".WithValue(type " +
      reflectlite.TypeOf(c.key).String() +
      ", val " + stringify(c.val) + ")"
}

这两个主要就是实现valueCtx的stringer的功能(打印父ctx和该ctx的type的类型和value的值),这里就不再赘述了。

cancelCtx:

type cancelCtx struct {
   Context

   mu       sync.Mutex            
   done     atomic.Value          
   children map[canceler]struct{} 
   err      error                 
}

cancelCtx除了实现Context的四种方法外,还拥有以下四个变量:

  • mu:主要是为了保护读写以下三个成员变量,而必须要有的同步锁
  • done:chan struct{}类型,首次读时才会初始化,当该context首次cancel时该管道关闭
  • children:cancelCtx会保存所有的可cancel的子context,当被cancel时处理完子ctx后会置为nil
  • err:cancelCtx首次cancel时会被置为non nil,并返回cancel原因信息

WithCancel()

首先来看下cancelCtx的初始化方法:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   c := newCancelCtx(parent)
   propagateCancel(parent, &c)
   return &c, func() { c.cancel(true, Canceled) }
}

这里的入参和valueCtx一样必须要传一个ctx作为该cancelCtx的父ctx

接着来看一下newCancelCtx()

func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent} // 这里比较奇怪,newXXX为啥不直接返回&cancelCtx{Context: parent}
}

这里实际上就只是把父ctx作为cancelCtx.Context成员变量然后创建cancelCtx。

propagateCancel()

然后就是propagateCancel(),该步骤主要是将该cancelCtx添加到父ctx的children当中:

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
   done := parent.Done()
   if done == nil {
      return // parent is never canceled
   }

   select {
   case <-done:
      // parent is already canceled
      child.cancel(false, parent.Err())
      return
   default:
   }

   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()
   } else {
      atomic.AddInt32(&goroutines, +1)
      go func() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

首先来分步骤解释:

done := parent.Done()
if done == nil {
   return 
}

首先获取parent.Done来获取父ctx的管道,这里主要是探测父ctx是否为能被cancel的context,若父ctx不能被cancel(done管道返回nil)那么就没有必要接下来的(将该cancelCtx添加到父ctx的children当中)操作了。

需要注意的是这里的父ctx是否为cancelCtx并不单单指该ctx的上一层的父ctx,也可能是祖父ctx或是更上层的可能出现的cancelCtx,例如:

image.png

虽然ctx2的父ctx:ctx1并不是可cancel的ctx,但ctx2.Done的返回的却不是nil

这是因为valueCtx并未实现Done()方法,这时如果ctx2去调用Done,则调用的是ctx1.Context.Done(),即ctx0的done(),因为ctx0为cancelCtx,所以这里返回的一定不是nil。

所以这里的 done == nil 语句可以看作是对该ctx所在的树的分支上是否拥有可cancel的ctx节点的判断。

接下来观察刚刚获取到的done管道:

select {
case <-done:
   child.cancel(false, parent.Err())
   return
default:
}

如果父ctx的管道已经被关闭了,那么这里的select就会走到case <-done:分支,这里会去直接调用cancelCtx的cancel()方法去cancel掉自己(之后介绍)。

走完上面两步,能够确认该cancelCtx的分支ctx中存在可cancel的ctx,且未被cancel,这时候就需要将该ctx添加到可cancel的父ctx的children下了。

那么如果该分支上存在多个可cancel的父ctx,这时候就需要找到离该ctx最近的一个可cancel的父ctx:

p, ok := parentCancelCtx(parent)

这里通过parentCancelCtx方法(后续介绍)获取到最近的一个可cancel的ctx

  • 如果找到了最近的一个可cancel的ctx:
p.mu.Lock()
if p.err != nil {
   child.cancel(false, p.err)
} else {
   if p.children == nil {
      p.children = make(map[canceler]struct{})
   }
   p.children[child] = struct{}{}
}
p.mu.Unlock()

首先判断该父ctx是否已经被cancel,如果关闭,则子ctx直接调用cancel()方法去cancel掉自己(这里又再次做了判断父ctx是否被关闭,主要是为了兜底在这段时间内其他goroutine可能对该父ctx做了关闭操作,因为之前操作未加锁)

若这里判断父ctx仍未被cancel掉,则最终将该子ctx存储到父ctx的map中,存储方式:子ctx作为children.Map的key值,value为空结构体。

  • 未找到最近的一个可cancel的ctx:
atomic.AddInt32(&goroutines, +1)
go func() {
   select {
   case <-parent.Done():
      child.cancel(false, parent.Err())
   case <-child.Done():
   }
}()

这种情况则单独创建一个goroutine去监控父ctx和当前ctx的cancel状态,如果父ctx先cancel,则当前的子ctx需要主动去cancel掉自己;若自己先cancel,则不做任何事,这里主要防止因为父ctx没有被cancel掉时,这个协程就会一直阻塞的情况。

parentCancelCtx()

现在来看一下ctx是怎么寻找到最近的父cancelCtx的:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   done := parent.Done()
   if done == closedchan || done == nil {
      return nil, false
   }
   p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
   if !ok {
      return nil, false
   }
   pdone, _ := p.done.Load().(chan struct{})
   if pdone != done {
      return nil, false
   }
   return p, true
}

这里首先会判断该ctx的done是否为closedchan(最近的cancelCtx已经被关闭),或是直接为nil(ctx的分支树上不存在可cancel的ctx),若上述条件任意成立一条,便认为未找到最近的可cancel的父ctx

这里的closedchan为一个可重用的全局channel,在context包初始化时就会被关闭,在之后的cancel()方法会提及该变量:

var closedchan = make(chan struct{})

func init() {
   close(closedchan)
}

接着查看该ctx的key为cancelCtxKey地址的Value:

p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)

其中cancelCtxKey的定义和获取Value的过程如下:

var cancelCtxKey int

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

在这里可以看到cancelCtx重写了Value(),并且在该方法中拦截了key为&cancelCtxKey的情况,若key为cancelCtxKey这个全局变量的地址时,则直接返回ctx自己。

为什么parent.Value(&cancelCtxKey)这里可以获取到最近的可cancel的父ctx?

这里可以举个例子:

image.png

例如ctx4为本次新创建的cancelCtx,则传入到parentCancelCtx()中的parent即为ctx3。

这时候到ctx3中找到key为&cancelCtxKey的Value值,因为ctx3中找不到该key(这里基本上不会出现key值和&cancelCtxKey值相同的情况),所以会去ctx3的父ctx:ctx2中继续调用Value()(原因上面已经讲过:Go源码学习:Context ),这时候会发现ctx2是cancelCtx,它会对key为&cancelCtxKey的情况进行拦截,这时候会返回自己(ctx2),所以ctx2就是距离ctx4最近的cancelCtx。

  • 如果遍历所有上层ctx后,仍未获取到key值为&cancelCtxKey的Value,则认为未找到最近的cancelCtx,这种情况只会发生在ctx链中只存在自定义的可cancel的Ctx
  • 如果取到了最近的cancelCtx:p,接着看后面的逻辑:
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
   return nil, false
}
return p, true

首先会获取最近的cancelCtx的done管道,这时候会拿该done管道和之前的parent.done来做对比:

  • 如果两个管道不是同一个管道,则也认为未找到最近的cancelCtx,但是这种情况又有点特殊,其实是找到了最近的可cancel的ctx,但该ctx为自定义的可cancel的ctx,例如: image.png 这里同样把ctx4作为本次新创建的cancelCtx,parent仍为ctx3。

这里的ctx2则是自定义的可cancel的ctx(称为custom cancelCtx),所以parent.done实际上就是ctx2.done。但这时候遍历ctx中key为&cancelCtxKey的Value,则最终会来到ctx0,所以上述代码中的p.done实际上为ctx0.done。

这样就很清楚了:ctx2.done != ctx0.done

所以说这里并非是没有找到最近的可cancel的ctx,而是找到了最近的可cancel的ctx为自定义ctx(custom cancelCtx)

context库之所以这么设计也是为了让自定义的cancelCtx能够自己管理sub cancelCtxs。

  • 最后如果判断两个done是同一个管道,那么则说明找到了最近的一个可cancel的ctx,且该ctx为预设的cancelCtx。

cancel()

在创建cancelCtx时,我们就能看到返回的变量中就存在cancel的逻辑:

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

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

这里会返回cancelFunc给业务方,业务方主动调用cancelFunc也就相当于调用context包下的cancel()方法

接下来看下cancelCtx的cancel()究竟做了些什么:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   if err == nil {
      // 首先cancel时必须要传cancel的原因,即error,否则直接panic
      panic("context: internal error: missing cancel error")
   }
   // 接着就是操作cancelCtx下的变量了,因为context本身是协程安全的,所以这里的操作都需要加锁。
   c.mu.Lock()
   if c.err != nil {
      // 走到这里说明当前ctx已经被cancel掉了
      c.mu.Unlock()
      return 
   }
   c.err = err
   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)
   }
   c.children = nil
   c.mu.Unlock()

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

以上步骤总结下来就是:

->加锁

->关闭done管道

->遍历sub cancelCtxs,将subCtx全部cancel掉

->将存储的subCtx的children置为nil

->若ctx需要从parent中移除,则调用removeChild()

->解锁

removeChild()

最后来看一下如何将subCtx从父ctx中移除掉:

func removeChild(parent Context, child canceler) {
   p, ok := parentCancelCtx(parent)
   if !ok {
      return
   }
   p.mu.Lock()
   if p.children != nil {
      delete(p.children, child)
   }
   p.mu.Unlock()
}

首先找到最近的cancelCtx(且最近的可cancel的ctx不是自定义ctx)

如果找到则将该cancelCtx中的child从map中删掉

timerCtx:

首先来看下timerCtx的结构:

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

   deadline time.Time
}

由结构可知,timerCtx也属于可cancel的ctx(只不过这里是通过dealine来自动cancel的),这里实际上继承了cancelCtx的Done和Err方法

WithDeadline()

创建timerCtx的方式有两种:WithTimeout()WithDeadline()

其中WithTimeout()最终会调用WithDeadline()

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

所以只需要看WithDeadline()即可:

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

继续拆解:

if cur, ok := parent.Deadline(); ok && cur.Before(d) {
   return WithCancel(parent)
}

这里主要是发现父ctx存在deadline,且父ctx的deadline在当前ctx的deadline之前,这种情况下创建的不再是timerCtx而是cancelCtx了,

接着:

c := &timerCtx{
   cancelCtx: newCancelCtx(parent),
   deadline:  d,
}
propagateCancel(parent, c)

首先创建timerCtx:将parentCtx作为c.cancelCtx.Context

然后把该cancelCtx添加到父ctx的children当中(propagateCancel()可参考cancelCtx中的内容)

next:

dur := time.Until(d)
if dur <= 0 {
   c.cancel(true, DeadlineExceeded) // deadline has already passed
   return c, func() { c.cancel(false, Canceled) }
}

如果这时候发现当前时间已经超过了dealine,这时候就会去主动调用timerCtx的cancel()(这里的cancel是timerCtx自己重写的方法,等会会介绍)去取消当前ctx。然后再返回该ctx

最后:

if c.err == nil {
   c.timer = time.AfterFunc(dur, func() {
      c.cancel(true, DeadlineExceeded)
   })
}
return c, func() { c.cancel(true, Canceled) }

这里的c.err实际上就是c.cancelCtx.err,若为nil则说明该ctx没有被cancel:

这时候设置计时器,在deadline时去调用timerCtx的cancel(),最后返回该ctx。

需要注意的是这里也会返回cancelFunc,说明timerCtx也是可以被手动cancel的,可根据ctx.Err()返回的信息来判断该ctx是被主动cancel还是因为计时器cancel:

  • "context deadline exceeded":计时器cancel
  • "context canceled":手动cancel

cancel()

timerCtx并没有使用cancelCtx的cancel,而是自己重写了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()
   if c.timer != nil {
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}

首先在该方法中会去先调用cancelCtx的cancel将初始化时传入的ctx给cancel掉

接着根据removeFromParent参数判断是否需要将该ctx从父ctx的children中移除

最后再将timer关掉

其他

  1. 可cancel的ctx:

这里说的可cancel的ctx指的是实现了下面interface的ctx类型:

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

只要实现了这两个方法,都可以看作是可cancel的ctx(duck typing)

所以可cancel的ctx包括且不限于:

  • cancelCtx
  • timerCtx
  • 自定义Ctx(实现了canceler)
  1. 可cancel的ctx中的children

cancelCtx和timerCtx都有children,children中存储的都是距离其最近的可cancel的ctx(包括自定义ctx)

但自定义的可cancel的ctx,可以没有children。但是也需要去管理它的子cancelCtx,毕竟当自己取消时也得取消子cancelCtx,至于如何去管理就需要自己来决定了。