context包内部如何实现的?

**题目来源:**好未来

答案1:

context是 Go 语言在 1.7 版本中引入标准库的接口。context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。

方法:

  • Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  • Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
  • Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
    • 如果 context.Context 被取消,会返回 Canceled 错误;
    • 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  • Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

设计原理

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

(context树结构)

78.context包内部如何实现的? - 图1

每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

如果不使用 context,当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗。

多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

默认上下文

context 包中最常用的方法还是 context.Background、context.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用。

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
  • context.TODO 应该仅在不确定应该使用哪种上下文时使用。
  1. func Background() Context {
  2. return background
  3. }
  4. func TODO() Context {
  5. return todo
  6. }
  7. 这两个私有变量都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体
  8. context.emptyCtx 的指针,这是最简单、最常用的上下文类型。
  9. ////////////////// emptyCtx ///////////////////
  10. type emptyCtx int
  11. func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
  12. return
  13. }
  14. func (*emptyCtx) Done() <-chan struct{} {
  15. return nil
  16. }
  17. func (*emptyCtx) Err() error {
  18. return nil
  19. }
  20. func (*emptyCtx) Value(key interface{}) interface{} {
  21. return nil
  22. }

取消信号

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

Context 子树的取消

  1. // context.WithCancel 函数的实现
  2. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  3. c := newCancelCtx(parent)
  4. propagateCancel(parent, &c)
  5. return &c, func() { c.cancel(true, Canceled) }
  6. }
  7. func propagateCancel(parent Context, child canceler) {
  8. done := parent.Done()
  9. if done == nil {
  10. return // 父上下文不会触发取消信号
  11. }
  12. select {
  13. case <-done:
  14. child.cancel(false, parent.Err()) // 父上下文已经被取消
  15. return
  16. default:
  17. }
  18. if p, ok := parentCancelCtx(parent); ok {
  19. p.mu.Lock()
  20. if p.err != nil {
  21. child.cancel(false, p.err)
  22. } else {
  23. p.children[child] = struct{}{}
  24. }
  25. p.mu.Unlock()
  26. } else {
  27. go func() {
  28. select {
  29. case <-parent.Done():
  30. child.cancel(false, parent.Err())
  31. case <-child.Done():
  32. }
  33. }()
  34. }
  35. }
  • context.newCancelCtx 将传入的上下文包装成私有结构体 context.cancelCtx;
  • context.propagateCancel 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:

    propagateCancel

propagateCancel 可能出现的情况:

  • 当 parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数会直接返回;
  • 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;
    • 如果已经被取消,child 会立刻被取消;
    • 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
  • 当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时;
    • 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;
    • 在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
      context.propagateCancel 的作用是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会出现状态不一致的情况。

context.cancelCtx 实现的几个接口方法也没有太多值得分析的地方,该结构体最重要的方法是 context.cancelCtx.cancel,该方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号

  1. // cancel 取消函数
  2. func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  3. c.mu.Lock()
  4. if c.err != nil {
  5. c.mu.Unlock()
  6. return
  7. }
  8. c.err = err
  9. if c.done == nil {
  10. c.done = closedchan
  11. } else {
  12. close(c.done)
  13. }
  14. for child := range c.children {
  15. child.cancel(false, err)
  16. }
  17. c.children = nil
  18. c.mu.Unlock()
  19. if removeFromParent {
  20. removeChild(c.Context, c)
  21. }
  22. }

context 包中的另外两个函数 context.WithDeadline 和 context.WithTimeout 也都能创建可以被取消的计时器上下文 context.timerCtx

  1. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  2. return WithDeadline(parent, time.Now().Add(timeout))
  3. }
  4. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  5. if cur, ok := parent.Deadline(); ok && cur.Before(d) {
  6. return WithCancel(parent)
  7. }
  8. c := &timerCtx{
  9. cancelCtx: newCancelCtx(parent),
  10. deadline: d,
  11. }
  12. propagateCancel(parent, c)
  13. dur := time.Until(d)
  14. if dur <= 0 {
  15. c.cancel(true, DeadlineExceeded) // 已经过了截止日期
  16. return c, func() { c.cancel(false, Canceled) }
  17. }
  18. c.mu.Lock()
  19. defer c.mu.Unlock()
  20. if c.err == nil {
  21. c.timer = time.AfterFunc(dur, func() {
  22. c.cancel(true, DeadlineExceeded)
  23. })
  24. }
  25. return c, func() { c.cancel(true, Canceled) }
  26. }

context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。
context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能

  1. ype timerCtx struct {
  2. cancelCtx
  3. timer *time.Timer // Under cancelCtx.mu.
  4. deadline time.Time
  5. }
  6. func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
  7. return c.deadline, true
  8. }
  9. func (c *timerCtx) cancel(removeFromParent bool, err error) {
  10. c.cancelCtx.cancel(false, err)
  11. if removeFromParent {
  12. removeChild(c.cancelCtx.Context, c)
  13. }
  14. c.mu.Lock()
  15. if c.timer != nil {
  16. c.timer.Stop()
  17. c.timer = nil
  18. }
  19. c.mu.Unlock()
  20. }

context.timerCtx.cancel 方法不仅调用了 context.cancelCtx.cancel,还会停止持有的定时器减少不必要的资源浪费。

传值方法

  1. func WithValue(parent Context, key, val interface{}) Context {
  2. if key == nil {
  3. panic("nil key")
  4. }
  5. if !reflectlite.TypeOf(key).Comparable() {
  6. panic("key is not comparable")
  7. }
  8. return &valueCtx{parent, key, val}
  9. }
  10. type valueCtx struct {
  11. Context
  12. key, val interface{}
  13. }
  14. func (c *valueCtx) Value(key interface{}) interface{} {
  15. if c.key == key {
  16. return c.val
  17. }
  18. return c.Context.Value(key)
  19. }

context.valueCtx 结构体会将除了 Value 之外的 Err、Deadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法,该方法的实现也很简单 。
如果 context.valueCtx 中存储的键值对与 context.valueCtx.Value 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。