关于golang:深度解析go-context实现原理及其源码

4次阅读

共计 7376 个字符,预计需要花费 19 分钟才能阅读完成。

目录

  • Context 根本应用办法
  • Context 应用场景
  • valueCtx

    • 应用示例
    • 构造体
    • WithValue
  • cancleCtx

    • 应用示例
    • 构造体
    • WitCancel
  • WithTimeout
  • WithDeadline

    • 应用示例
    • WithDeadline
  • 总结

Context 根本应用办法

首先,咱们来看一下 Context 接口蕴含哪些办法,这些办法都是干什么用的。

包 context 定义了 Context 接口,Context 的具体实现包含 4 个办法,别离是 Deadline、Done、Err 和 Value,如下所示:

type Context interface {Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{} Err()
  error 
  Value(key interface{}) interface{}}

Deadline 办法会返回这个 Context 被勾销的截止日期。如果没有设置截止日期,ok 的值是 false。后续每次调用这个对象的 Deadline 办法时,都会返回和第一次调用雷同的后果。

Done 办法返回一个 Channel 对象。在 Context 被勾销时,此 Channel 会被 close,如果没被勾销,可能会返回 nil。后续的 Done 调用总是返回雷同的后果。当 Done 被 close 的时候,你能够通过 ctx.Err 获取错误信息。Done 这个办法名其实起得并不好,因为名字太过抽象,不能明确反映 Done 被 close 的起因,因为 cancel、timeout、deadline 都可能导致 Done 被 close,不过,目前还没有一个更适合的办法名称。

对于 Done 办法,你必须要记住的知识点就是:如果 Done 没有被 close,Err 办法返回 nil;如果 Done 被 close,Err 办法会返回 Done 被 close 的起因。

Context 应用场景

  • 上下文信息传递(request-scoped),比方解决 http 申请、在申请解决链路上传递信息
  • 管制子 goroutine 的运行
  • 超时管制的办法调用
  • 能够勾销的办法调用

valueCtx

valueCtx 是基于 parent Context 生成一个新的 Context,保留了一个 key-value 键值对。它次要用来传递上下文信息。

应用示例

ctx := context.Background()
ctx = context.WithValue(ctx, "key1", "0001")
ctx = context.WithValue(ctx, "key2", "0001")
ctx = context.WithValue(ctx, "key3", "0001")
ctx = context.WithValue(ctx, "key4", "0004")
fmt.Println(ctx.Value("key1")) // 0001

查找过程如图所示:

构造体

type valueCtx struct {
   Context  // parent Context
   key, val interface{}  // key-value}

func (c *valueCtx) Value(key interface{}) interface{} {
   // 若 key 值 等于 以后 valueCtx 存储的 key 值 
   // 则取出其 value 并返回
   if c.key == key {return c.val}
   // 否则递归调用 valueCtx 中 Value 办法,获取其 parent Context 中存储的 key-value
   return c.Context.Value(key)
}

通过观察 valueCtx 构造体,它利用一个 Context 变量示意其父节点的 context,这样 valueCtx 也继承了父节点的所有信息;并且它持有一个 key-value 键值对,阐明它还能够携带额定的信息。它还笼罩了 Value 办法,优先从本人的存储中查看这个 key,不存在的话会从 parent 中持续查看。

WithValue

WithValue 就是向 context 中增加键值对:

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

通过代码能够看出,向 context 中增加键值对并不是在原 context 根底上增加的,而是新建一个 valueCtx 子节点,将原 context 作为父节点。以此类推,就会造成一个 context 链。在查找过程中,如果以后 valueCtx 不存在 key 值,还会向 parent Context 去查找,如果 parent 还是 valueCtx 的话,还是遵循雷同的准则:valueCtx 会嵌入 parent,所以还是会查找 parent 的 Value 办法的。

cancleCtx

在咱们开发过程中,咱们经常会遇到一些场景,须要被动勾销长时间的工作或者停止工作,这个时候就能够应用 cancelCtx。通过调用 cancel 函数就可停止 goroutine,进而去开释所占用的资源。

须要留神的是,不是只有中途停止工作时才调用 cancel 函数,只有工作执行结束后,就须要调用 cancel,这样,这个 Context 能力开释它的资源(告诉它的 children 解决 cancel,从它的 parent 中把本人移除,甚至开释相干的 goroutine)。

应用示例

func main() {
  // gen 在独自的 goroutine 中生成整数 而后将它们发送到返回的管道
  gen := func(ctx context.Context) <-chan int {dst := make(chan int)
     n := 1
     go func() {
        for {
           select {case <-ctx.Done():
              return // returning not to leak the goroutine
           case dst <- n:
              n++
           }
        }
     }()
     return dst
  }
  ctx, cancel := context.WithCancel(context.Background())
  // 代码结束后调用 cancel 函数开释 goroutine 所占用的资源
  defer cancel() // cancel when we are finished consuming integers
  // 遍历循环获取管道中的值
  for n := range gen(ctx) {fmt.Println(n)
     if n == 5 {break}
  }
}

创立一个 gen 函数,在 gen 函数中创立一个 goroutine,专门用来生成整数,而后将他们发送到返回的管道。通过 context.WithCancel 创立可勾销的 context,最初遍历循环获取管道中值,当 n 的值为 5 时,退出循环,完结过程。最初调用 cancel 函数开释 goroutine 所占用的资源。

构造体

type cancelCtx struct {
    Context
    mu       sync.Mutex            
    done     chan struct{}         
    children map[canceler]struct{} 
    err      error                 
}

cancelCtx 和 valueCtx 相似,构造体中都有一个 Context 作为其父节点;变量 done 示意敞开信号传递;变量 children 示意以后节点所领有的子节点,err 用于存储错误信息示意工作完结的起因。

接下来,看看 cancelCtx 实现的办法:

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

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

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   if err == nil {panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()
   if c.err != nil {c.mu.Unlock()
      return // already canceled
   }
   c.err = err
   // 设置一个敞开的 channel 或者将 done channel 敞开,用以发送敞开信号
   if c.done == nil {c.done = closedchan} else {close(c.done)
   }
   // 遍历循环将字节点 context 勾销
   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 {
      // 将以后 context 节点从父节点上移除
      removeChild(c.Context, c)
   }
}

cancelCtx 构造体实现 Done 和 cancel 办法,Done 办法实现了将 done 初始化。cancel 办法用于将以后节点从父节点上移除以及移除以后节点下的 所有子节点。

cancelCtx 被勾销时,它的 Err 字段就是上面这个 Canceled 谬误:

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

WithCancel

WithCancel 函数用来创立一个可勾销的 context,即 cancelCtx 类型的 context。

WithCancel 函数返回值有两个,一个为 parent 的正本 Context,另一个为触发勾销操作的 CancelFunc。

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

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
   // 将 parent 作为父节点 context 生成一个新的子节点
   return cancelCtx{Context: parent}
}

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:
   }
   
   // 获取最近的类型为 cancelCtx 的先人节点
   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{})
         }
         // 将以后子节点退出最近 cancelCtx 先人节点的 children 中
         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():}
      }()}
}

调用 WithCancel 函数时,首先会调用 newCancelCtx 函数创立一个以 parent 作为父节点的 context。而后调用 propagateCancel 函数,用来建设以后 context 节点与 parent 节点之间的关系。

在 propagateCancel 函数中,如果 parent 节点为 nil,阐明 parent 以上的门路没有可勾销的 cancelCtx,则不须要解决。

否则通过 parentCancelCtx 函数过来以后节点最近的类型为 cancelCtx 的先人节点,首先须要判断该先人节点是否被勾销,若已被勾销就勾销以后节点;否则将以后节点退出先人节点的 children 列表中。

否则的话,则须要新起一个 goroutine,由它来监听 parent 的 Done 是否已敞开。一旦 parent.Done() 返回的 channel 敞开,即 context 链中某个先人节点 context 被勾销,则将以后 context 也勾销。

WithTimeout

WithTimeout 其实是和 WithDeadline 一样,只不过一个参数是超时工夫,一个参数是截止工夫。超时工夫加上以后工夫,其实就是截止工夫,因而,WithTimeout 的实现是:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 
  // 以后工夫 +timeout 就是 deadline
  return WithDeadline(parent, time.Now().Add(timeout))
}

WithDeadline

WithDeadline 会返回一个 parent 的正本,并且设置了一个不晚于参数 d 的截止工夫,类型为 timerCtx(或者是 cancelCtx)。

如果它的截止工夫晚于 parent 的截止工夫,那么就以 parent 的截止工夫为准,并返回一个类型为 cancelCtx 的 Context,因为 parent 的截止工夫到了,就会勾销这个 cancelCtx。

如果以后工夫曾经超过了截止工夫,就间接返回一个曾经被 cancel 的 timerCtx。否则就会启动一个定时器,到截止工夫勾销这个 timerCtx。

综合起来,timerCtx 的 Done 被 Close 掉,次要是由上面的某个事件触发的:

  • 截止工夫到了
  • cancel 函数被调用
  • parent 的 Done 被 close

应用示例

func main() {d := time.Now().Add(time.Second * 3)
  ctx, cancel := context.WithDeadline(context.Background(), d)
  defer cancel()
  select {case <-time.After(3 * time.Second):
     fmt.Println("overslept")
  case <-ctx.Done():
     fmt.Println(ctx.Err())
  }
}

WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if parent == nil {panic("cannot create context from nil parent")
   }
   // 如果 parent 的截止工夫更早,间接返回一个 cancelCtx 即可
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {return WithCancel(parent)
   }
   c := &timerCtx{cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   // 建设新建 context 与可勾销 context 先人节点的勾销关联关系
   propagateCancel(parent, c)
   dur := time.Until(d)
   if dur <= 0 { // 以后工夫曾经超过了截止工夫,间接 cancel
      c.cancel(true, DeadlineExceeded) 
      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) }
}

调用 WithDeadline 函数,首先判断 parent 的截止工夫是否早于以后 timerCtx,若为 true 的话,间接返回一个 cancelCtx 即可。否则须要调用 propagateCancel 函数倡议新建 context 与可勾销 context 先人节点的勾销关联关系,建设关联关系之后,若以后工夫曾经超过截止工夫后,间接 cancel。否则的话,需设置一个定时器,到截止工夫后勾销。

总结

context 次要用于父子工作之间的同步勾销信号,实质上是一种协程调度的形式。另外在应用 context 时有两点值得注意:上游工作仅仅应用 context 告诉上游工作不再须要,但不会间接干预和中断上游工作的执行,由上游工作自行决定后续的解决操作,也就是说 context 的勾销操作是无侵入的;context 是线程平安的,因为 context 自身是不可变的(immutable),因而能够释怀地在多个协程中传递应用。

到这里,Context 的源码已解读结束,心愿对您有播种,咱们下期再见。

文章也会继续更新,能够微信搜寻「迈莫 coding」第一工夫浏览。每天分享优质文章、大厂教训、大厂面经,助力面试,是每个程序员值得关注的平台。

正文完
 0