跳转至

context原理

context是什么

context是go语言在1.7引入的一个用于goroutine之间传递信息的并发安全的包,context可以翻译为上下文,其在项目中主要是用于上下与下层goroutine的取消控制以及数据共享,也是go语言中goroutine之间通信的一种方式,其底层是借助channlsync.Mutex实现的。

关于context的用法我们在前一章节并发实践里已经做过介绍,本章主要介绍一下context的底层原理。

context的底层实现

与context相关的源码基本都在src/context/context.go中,我们通过源码来看一下,context的底层究竟做了些什么。

context在底层实现上其实用到了2个接口,对这个接口的4种实现,以及提供了6个方法:

接口:

接口名 说明
Context context的接口定义,规定context的实现必须包含的四个基本方法
canceler context的取消接口,其中定义了两个方法

实现:

context接口的四种实现

结构名 说明
emptyCtx 一个空的context,用作根context
cancelCtx 可以通过取消函数来取消context
timerCtx 可以通过定时器和deadline来定时取消context
valueCtx 类似于map,可以用来存储key/value键值对

方法:

函数名 说明
Background 返回一个根context即emptyCtx
TODO 也是返回一个根context即emptyCtx
WithCancel 派生出一个cancelCtx
WithDeadline 派生出一个timerCtx
WithTimeout 派生出一个timerCtx
WithValue 派生出一个valueCtx

下面我们将逐一解读这几个结构及其实现方法。

接口说明

context接口

首先还是回顾一下context接口,context的接口定义如下:

Go
1
2
3
4
5
6
type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}

接口提供了四个方法:

  • Deadline: 返回 context.Context 被取消的时间,即截止时间;

  • Done: 返回一个 channel,当Context被取消或者到达截止时间,这个 channel 就会被关闭,表示context结束,多次调用 Done 方法返回的channel是同一个;

  • Err: 返回 context.Context 结束的原因;

  • Value :从 context.Context 中获取键对应的值,类似于map的 get 方法,对于同一个context,多次调用 Value 并传入相同的 key 会返回相同的结果,如果没有对应的 key,则返回 nil,键值对是通过 WithValue 方法写入。

canceler接口

canceler接口的源码定义如下:

Go
1
2
3
4
type canceler interface {
   cancel(removeFromParent bool, err error)  // 创建cancel接口实例的goroutine 调用cancel方法通知被创建的goroutine退出
   Done() <-chan struct{}  // 返回一个channel,后续被创建的goroutine通过监听这个channel的信号来完成退出
}

canceler接口主要用于取消方法的实现,如果一个示例既实现了context接口又实现了canceler接口,那么这个context就是可以本取消的,比如cancelCtxtimerCtx。如果仅仅只是实现了context接口,而没有实现canceler,就是不可取消的,比如emptyCtxvalueCtx

context实现

在context包下对context接口有四种基本的实现,即emptyCtxcancelCtxtimerCtxvalueCtx

emptyCtx

首先看一下emptyCtx这个最基本的实现,emptyCtx虽然实现了context接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值。虽然emptyCtx没有任何功能,但它还是有作用的,一般用它作为根context来派生出有实际用处的context。要想创建有实际功能的context,要使用后续提供的一系列with方法来派生出新的context,这个在前面讲context用法的时候已经做过介绍,就不再过多赘述。

emptyCtx的相关源码:

Go
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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 any) any {
   return nil
}

可以看到emptyCtx的实现没有做任何操作,就是一个整形结构。这个空的emptyCtx会在两个创建根context的函数中被用到:

Go
1
2
3
4
5
6
7
func Background() Context {
   return background
}

func TODO() Context {
   return todo
}

而这里BackgroundTODO其实就是返回一个emptyCtx

Go
1
2
3
4
var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)

在开发中,调用这两个函数Background()或者TODO()创建最顶层的context其实就是获取一个emptyCtx

cancelCtx

cancelCtx结构定义如下:

Go
1
2
3
4
5
6
7
8
9
type cancelCtx struct {
   Context                         // 组合了一个Context ,所以cancelCtx 一定是context接口的一个实现
   mu       sync.Mutex             // 互斥锁,用于保护以下三个字段
   // value是一个chan struct{}类型,原子操作做锁优化
   done     atomic.Value          
   // key是一个取消接口的实现,map其实存储的是当前canceler接口的子节点,当前context被取消时,会遍历子节点发送取消信号
   children map[canceler]struct{}  
   err      error                  // context被取消的原因
}

下面看一下其各个方法的具体实现,首先看一下Done()方法:

Go
func (c *cancelCtx) Done() <-chan struct{} {
   d := c.done.Load()
   if d != nil {
      return d.(chan struct{})
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   d = c.done.Load()
   if d == nil {
      d = make(chan struct{})
      c.done.Store(d)
   }
   return d.(chan struct{})
}

代码很简单,其实就是采用“懒汉模式”创建一个struct{}类型的channel返回,从类型可以看出这个channel是只读的,不能往里面写数据,所以应该避免直接读取这个channel,会发生阻塞。所以在使用上要配合select来非阻塞读取,由于是只读的,所以只有在一种情况下会读到值,那就是关闭这个channel的时候会读到零值。利用这个而特性就可以实现关闭的消息通知。

再看一下其 cancel() 方法的实现:

Go
// 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 {   // context被取消的原因,必传,否则panic
      panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()
   if c.err != nil {  // 在赋值这个err之前,c.err已经有值了,说明已经被调用过cancel函数了,c这个context已经被取消
      c.mu.Unlock()
      return // already canceled
   }
   c.err = err       // 赋值err信息
   d, _ := c.done.Load().(chan struct{})  // 获取通知管道
   if d == nil {                        
      c.done.Store(closedchan)
   } else {
      close(d)                           // 关闭管道          
   }
   // 遍历当前context的所有子节点,调用取消函数
   for child := range c.children {
      // NOTE: acquiring the child's lock while holding parent's lock.
      child.cancel(false, err)  // 递归取消子context
   }
   c.children = nil  // 取消动作完成之后,孩子节点置空
   c.mu.Unlock()

   if removeFromParent {
      removeChild(c.Context, c)  // 将自身从父节点children map种移除
}

cancel不仅取消当前context,还会遍历当前context的所有子context,递归取消,递归取消玩当前context的所有子context后,会将自身从父节点children map中移除,移除函数removeChild源码如下:

Go
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
   p, ok := parentCancelCtx(parent)
   if !ok {
      return
   }
   p.mu.Lock()
   if p.children != nil {
      delete(p.children, child)   // 从父context的children中移除
   }
   p.mu.Unlock()
}

移除前后效果如下图所示:

img

在用户层面,创建cancelCtx的方法其实我们你之前也接触过,就是withCancel方法,在平常代码中,我们一般用这个方法来派生一个可以用cancel取消函数取消的context,常规用法如下:

Go
ctx, cancel := context.WithCancel(context.Background())

下面继续跟一下这个WithCancel函数的源码:

Go
1
2
3
4
5
6
7
8
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   if parent == nil { // 传入的父context不能为空,否则报panic
      panic("cannot create context from nil parent")
   }
   c := newCancelCtx(parent)   // 这里就会创建一个cancelCtx
   propagateCancel(parent, &c)   // 这里主要是关联父context ctx和子congtxt c的逻辑
   return &c, func() { c.cancel(true, Canceled) }  // 具体的取消函数cancel的实现
}

前面说了调用cancelFunc函数可以级联取消子context,那么为什么可以级联取消呢?propagateCancel函数就是用来做这个工作的,他将父context和子context关联起来,具体的关联逻辑,我们通过源码来分析:

Go
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
   done := parent.Done()  // 获取父context的通信管道 chan struct{}
   if done == nil {     // done为空,说明父context不会被取消
      return // parent is never canceled
   }

   select {
   case <-done:     // 通信管道收到了消息,说明父context已经被取消,不用重复取消了
      // parent is already canceled
      child.cancel(false, parent.Err())  // 但是父context已经取消,这里子context也应该要取消,由于还没有关联上,所以主动调用cancel取消关联
      return
   default:
   }

   if p, ok := parentCancelCtx(parent); ok {   // 从父context中提取出cancelCtx结构
      p.mu.Lock()
      if p.err != nil {   // 加锁后双重检查,再次检查父context有没有被取消
         // parent has already been canceled
         child.cancel(false, p.err)  // 父context被取消,主动取消子context
      } else {                       // 父context没有被取消
         if p.children == nil {     
            p.children = make(map[canceler]struct{})   // 创建父context的children map
         }
         p.children[child] = struct{}{}               // 把当前子context加入到children map里面
      }
      p.mu.Unlock()
   } else {                             //   从父context中没有提取出cancelCtx结构
      atomic.AddInt32(&goroutines, +1)
      go func() {                       // 新起一个goroutine监控父子context的通信管道有没有取消信号       
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

看一下这个提取父context的cancelCtx结构的parentCancelCtx方法:

Go
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   done := parent.Done()  
    // 从父context的取消信息管道为空,说明父context不会被取消
    // closedchan is a reusable closed channel.
    // var closedchan = make(chan struct{})
    // done == closedchan,表明ctx不是标准的 cancelCtx,可能是自定义的结构实现了 context.Context 接口
   if done == closedchan || done == nil {  // 
      return nil, false
   }
   p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)  // 通过context的value方法从父context中提取出cancelCtx
   if !ok {
      return nil, false
   }
   pdone, _ := p.done.Load().(chan struct{}) // 判断父context里的通信管道和cancelCtx里的管道是否一致
   if pdone != done {  // 不一致,表明parent不是标准的cancelCtx
      return nil, false
   }
   return p, true   // 返回cancelCtx
}

总结一下通过WithCancel函数在派生可取消的子context的过程中,通过propagateCancel函数关联父子context可能遇到的几种情形:

  1. 父context的通信管道done为空或者已经被取消,就不用关联了,直接取消当前子context即可;

  2. 父context可以被取消,但是还未被取消,并且父context可以提取出标准的cancelCtx结构,则创建父context的children map,将当前子context加入到这个map中;

  3. 父context可以被取消,但是还未被取消,父context不能提取出标准的cancelCtx结构,新起一个goroutine监控父子context的通信管道有没有取消信号。

timerCtx

timerCtxcancelCtx的基础上,又提供了截止时间的功能,不仅拥有像cancelCtx 一样,可以通过调用取消函数cancelFun来取消子context的方式,还可以设置一个截止时间deadline ,在deadline到来时,自动取消context。

首先看一下timerCtx的结构定义:

Go
1
2
3
4
5
type timerCtx struct {
   cancelCtx
   timer *time.Timer // Under cancelCtx.mu.
   deadline time.Time
}

看到它内置了cancelCtx,所以cancelCtx拥有的功方法,他可以调用cancelCtx的方法,能够主动取消context,再看一下timerCtx自身的cancel方法实现:

Go
func (c *timerCtx) cancel(removeFromParent bool, err error) {
   c.cancelCtx.cancel(false, err)    // 直接调用cancelCtx的cancel方
   if removeFromParent {
      // Remove this timerCtx from its parent cancelCtx's children.
      removeChild(c.cancelCtx.Context, c)  // 将当前子context从父context中删除
   }
   c.mu.Lock()
   if c.timer != nil {   // 要关闭掉定时器,因为手动取消过一次了,如果不关闭,在deadline到来时,不会再次取消,造成错误
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}

同样在用户层面,我们一般通过WithTimeout或者WithDeadline来创建一个timerCtx

Go
 ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second))  // 截止时间当前时间4s后
 ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)   // 超时时间为4s后

WithTimeout内部其实也是调用了WithDeadline,所以只用分析WithDeadline方法即可:

Go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if parent == nil {  // 父context为空,直接报panic
      panic("cannot create context from nil parent")
   }
   // 如果父context的deadline早于这里要设置的子context的截止时间
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      // 直接取消父context即可,不需要再管子context的取消时间,直接构建一个可以取消的子context
      // 因为父context的到期时间早于子context,当父context被取消的时候,这个子context肯定会被级联取消
      return WithCancel(parent)
   }
   // 创建timerCtx对象
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c)  // 关联父子context
   dur := time.Until(d)        // 获取距离设置的子context过期时间的时间差
   if dur <= 0 {               // 时间差小于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 {
     // 根据时间差,创建一个定时器,到deadline的时候定时触发取消
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

所以,父context未取消的情况下,在创建timerCtx的时候有两种情况:

  • 设置的截止时间晚于父context的截止时间,则不会创建timerCtx,会直接创建一个可取消的context,因为父context的截止时间更早,会先被取消,父context被取消的时候会级联取消这个子context;

  • 设置的截止时间早于父context的截止时间,会创建一个正常的timerCtx

valueCtx

valueCtx的作用与上述三个context有点不同,它不是用于父子context之间的取消的,而是用于数据共享。

作用类似于一个map,不过数据的存储和读取是在两个context,用于goroutine之间的数据传递。

valueCtx的结构定义如下:

Go
1
2
3
4
type valueCtx struct {
    Context
    key, val interface{}
}

valueCtx内置了Context,所以他也是一个context接口的实现,但是其没有实现canceler接口,所以他不能用作context的取消,valueCtx实现了String()方法和Value方法,String()比较简单,就不细看了。

下面看一下Value方法:

Go
1
2
3
4
5
6
func (c *valueCtx) Value(key interface{}) interface{} {
   if c.key == key {
      return c.val
   }
   return c.Context.Value(key)
}

方法很简单,就是向上递归的查找key所对应的value,如果找到则直接返回value,否则查找该context的父context,一直顺着 context向上,最终找到根节点(一般是emptyCtx),直接返回一个nil。查找过程如下图:

img

从定义可以出valueCtx中存储着一对键值对,具体是怎么用的呢?同样我们一般使用withValue方法派生出一个valueCtx

Go
ctx := context.WithValue(context.Background(),"key1","value1")

withValue函数源码如下:

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

withValue的方法实现很简单,就是创建一个valueCtx,将keyvalue设置到valueCtx返回。