Tomyang_

V1

2022/07/12阅读:24主题:前端之巅同款

go源码之context.Context

本文基于Go版本:1.17.8

go version go1.17.8 darwin/amd64 

什么是Context

  • Go 1.7标准引入Context,中文翻译为:上下文,应该准备来说它是协程(goroutine)的上下文,包含当前会话协程(goroutine)的运行状态、环境、当前现场信息
  • Context主要用来在协程(goroutine)之间传递上下文信息, 取消信号(signal)、设置超时时间(timeout)、存储当前会话Key-Value键值对等。
  • 随着引入Context包,标准库中很多接口把首位参数都留给了Context,它几乎成为了并发控制与超时控制的标准做法。

context.Context类型的值可以协调协程(goroutine)中的代码执行取消操作,并且可以存储当前会话的键值对,最重要是它是并发安全的

Context 在一组goroutine之间传递共享值、取消信号、Deadline
Context 在一组goroutine之间传递共享值、取消信号、Deadline

Context 底层实现原理

Context 接口
type Context interface {
  // 当Context 被取消或者到了deadline时,就会返回一个被关闭的 channel
  Done() <- chan struct{}
  // 在channel Done关闭后, 返回context取消的错误信息
  Err() error
  // 返回 context 是否被取消以及自动取消时间
  Deadline()(deadline time.Time, ok bool)
  // 获取key 对应的 value
  Value(key interface{}) interface{}
}
  • Context 是一个接口(interface),定义了4个方法(func),它们都是幂等的。如果连续多次调用同一个方法,得到的结果都是相同的。
  • Done() 返回一个channel,可以表示context被取消的信号: 当返回的channel被关闭时,已说明context被取消,注意!返回的channel是一个只读的,当读一个关闭的channel会读出相应类型的零值。在源码里没有地方向这个channel里面塞值,因此子协程(goroutine)读这个channel,除非channel被关闭,否则读不出来任何值,也是利用了这一点,子协程(goroutine)从channel里读出了零值后,子协程(goroutine)就可以做一些收尾工作,退出当前工作状态。
  • Err() 返回一个错误,表示channel 被关闭的原因, 比如被取消或超时。
  • Deadline() 返回context的截止时间, 通过此time.Time, 函数就可以决定是否还继续接下来的操作,如果时间太短,终止当前任务执行。
  • Value() 获取之前设置的Key对应的Value
Canceler 接口
type canceler interface{
  cancel(removeFromParent bool, err error)
  Done()<-chan struct{}
}

实现上面接口的两个函数的Context,就说明Context是可以取消操作的。源码有两个类型实现的canceler接口:*cancelCtx*timerCtx。加了*号是这两个结构体的指针实现了canceler接口。

type cancelCtx struct {
  Context
  mu  sync.Mutex
  done atomic.Value //惰性创建channel,由第一次取消调用关闭
}

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.Loca()
  if d == nil {
    d = make(chan struct{})
    c.done.Store(d)
  }
  return d.(chan struct{})
}

c.done是惰性创建的,只有调用了Done()方法时才会被创建。函数创建一个只读Channel,而且没有地方向这个Channel里面写数据。所以直接调用读channel,协程(goroutine)会被block住,一般搭配select来使用。一旦关闭,就立即读取零值

var closedchan = make(chan struct{})

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // 必须要传 err
 if err == nil {
  panic("context: internal error: missing cancel error")
 }
 c.mu.Lock()
 if c.err != nil {
  c.mu.Unlock()
  return // 已经被其他协程取消
 }
 // 给 err 字段赋值
 c.err = err
  d,_ := c.done.Load().(chan struct{})
 if d == nil{
    c.done.Store(closedchan)
  }else{
    close(d)
  }
 // 遍历它的所有子节点
 for child := range c.children {
     // 递归地取消所有子节点
  child.cancel(false, err)
 }
 // 将子节点置空
 c.children = nil
 c.mu.Unlock()
 if removeFromParent {
     // 从父节点中移除自己 
  removeChild(c.Context, c)
 }
}

cancel()方法的功能就是关闭Channel

  • 遍历取消它所有子节点,从父节点删除,通过关闭Channel,将取消信号产地給它所有的子节点,协程(goroutine)接到取消信号后就使用select语句读取c.Done()
func propagateCancel(){
(...)
go func(){
  select {
    case <-parent.Done():
      child.cancel(false, parent.Err())
    case <-child.Done():
  }
}
}
Context树
Context树

如上图,代表一个Context树,当调用左图中标红Contextcancel方法后,该Contex会从它的父Context中去除掉,实线变成虚线,且虚线圈出来的Context都会被取消,圈内的Context间的父子关系都会不存在。 重点看propagateCancel():

func propagateCancel(parent Context, child canceler) {
  //父节点是个空节点
 done := parent.Done()
 if done == nil {
  return
 }
  //监听父节点是否取消
 select {
 case <-done:
  child.cancel(false, parent.Err())
  return
 default:
 }
  // 找到可以取消的父节点
 if p, ok := parentCancelCtx(parent); ok {
  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()
 } else {
    //惰性加载第一次
  atomic.AddInt32(&goroutines, +1)
    //如果没有找到可取消的父 context. 新启动一个协程监控父接地那或者子节点去取消的信号
  go func() {
   select {
   case <-parent.Done():
    child.cancel(false, parent.Err())
   case <-child.Done():
   }
  }()
 }
}
TimerCtx

timeCtx基于cancelCtx,只是多了一个time.Timer(定时器)和一个deadline。timer 会在deadline到来时,自动取消Context

type timeCtx struct {
  cancelCtx
  timer *time.Timer 
  deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
  // 直接调用 cancelCtx 的取消方法
 c.cancelCtx.cancel(false, err)
 if removeFromParent {
  // 从父节点中删除子节点
  removeChild(c.cancelCtx.Context, c)
 }
 c.mu.Lock()
 if c.timer != nil {
    // 停止定时器,这样 在deadline 到来时,不会再次进行取消
  c.timer.Stop()
  c.timer = nil
 }
 c.mu.Unlock()
}

创建timerCtx的方法

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

WithTimeout 函数直接调用WithDeadline,传入deadline 是当前时间加上timeout的时间,也就是从现在开始再经过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) {
  // 如果父节点Context的deadline 早于指定时间,直接构建一个可取消的context
    // 原因是一旦父节点超时,自动调用 cancel 函数, 子节点也会随之取消
    // 所以不用单独处理子节点的计时器到了之后,自动调用cancel函数
  return WithCancel(parent)
 }
  // 创建timerCtx
 c := &timerCtx{
  cancelCtx: newCancelCtx(parent),
  deadline:  d,
 }
  // 挂靠到父节点上
 propagateCancel(parent, c)
  // 计算当前距离deadline 的时间
 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 {
    // dur 时间后 timer 会自动调用 cancel 函数.自动取消
  c.timer = time.AfterFunc(dur, func() {
   c.cancel(true, DeadlineExceeded)
  })
 }
 return c, func() { c.cancel(true, Canceled) }
}

把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消,该函数最核心的代码

c.timer = time.AfterFunc(dur,func(){
  c.cancel(true, DeadlineExceeded)
})
valueCtx
type valueCtx struct {
 Context
 key, value interface{}
}

实现两个方法

func (c *valueCtx) String() string {
 return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

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

它直接将Context作为匿名字段,因此它只需要实现2个方法,其它方法继承了Context,但它任然是一个Context创建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}
}

valueCtx树形结构 Context 指向它父节点,通过WithValue 函数,可以创建层层的valueCtx,存储协程(goroutine)间可以共享的变量

传递共享数据
//示列
type activityKey struct{}

func NewWithConfigContext(ctx context.Context, act interface{}) context.Context {
 return context.WithValue(ctx, activityKey{}, act)
}
func FromWithConfigContext(ctx context.Context, actType int32) interface{} {
 if data, ok := ctx.Value(activityKey{}).(interface{}); ok {
    return data
 }
 return nil
}
在gin 框中使用
// 在gin中间件引入 requestID
func WithRequestID() gin.HandlerFunc {
 return func(c *gin.Context) {
  requestID := util.UUID()
  c.Request =  c.Request.WithContext(NewWithConfigContext(c.Request.Context(), requestID))
  c.Next()
 }
}

分类:

后端

标签:

Golang

作者介绍

Tomyang_
V1

Golang