golang context 原理解析

1,181 阅读5分钟

图片.png context的中文翻译是上下文 ,我们可以理解为 context 管理了一组呈现树状结构的 Goroutine ,让每个Goroutine 都拥有相同的上下文,并且可以在这个上下文中传递数据

我们理解为:当一个goroutine有很多子goroutine是,如果某事goroutine失败了,如何通知全部g取消呢,就是通过context

接下来看几个案例,再看原理

context 实际上只是定义的4个方法的接口,凡是实现了该接口的都称为一种 context

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
    // 返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。
	Deadline() (deadline time.Time, ok bool)
    // 绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。
	Done() <-chan struct{}
    // 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。
	Err() error
    // 获取上游Goroutine 传递给下游Goroutine的某些数据
	Value(key interface{}) interface{}
}

context.go 包中提供了4个以 With 开头的函数, 这几个函数的主要功能是实例化不同类型的context

1、通过 Background() 和 TODO() 创建最 emptyCtx 实例 ,通常是作为根节点

2、通过 WithCancel() 创建 cancelCtx 实例

3、通过 WithValue() 创建 valueCtx 实例

4、通过 WithDeadline 和 WithTimeout 创建 timerCtx 实例

WithCancel
package main

import (
   "context"
   "fmt"
   "time"
)

func MyDo2(ctx context.Context) {
   for {
      select {
      default:
         fmt.Println("MyDo2 doing.... ")
         time.Sleep(2 * time.Second)
      case <-ctx.Done():
         fmt.Println("MyDo2 Done")
         fmt.Println(ctx.Err())
         return
      }
   }

}
func MyDo1(ctx context.Context) {
   go MyDo2(ctx)
   for {
      select {
      case <-ctx.Done():
         fmt.Println("MyDo1 Done")
         fmt.Println(ctx.Err())
         return
      default:
         fmt.Println("MyDo1 doing....  ")
         time.Sleep(1 * time.Second)
      }
   }
}
func main() {
   // 创建 cancelCtx 实例
   // 传入context.Background() 作为根节点
   ctx, cancel := context.WithCancel(context.Background())
   // 向协程中传递ctx
   go MyDo1(ctx)
   time.Sleep(7 * time.Second)
   fmt.Println("stop all goroutines")
   // 执行cancel操作
   cancel()
   time.Sleep(2 * time.Second)

}

图片.png

WithDeadline
package main

import (
   "context"
   "fmt"
   "time"
)

func dl2(ctx context.Context) {
   n := 1
   for {
      select {
      case <-ctx.Done():
         fmt.Println(ctx.Err())
         return
      default:
         fmt.Println("dl2 doing....  ", n)
         n++
         time.Sleep(time.Second)
      }
   }
}

func dl1(ctx context.Context) {
   n := 1
   for {
      select {
      case <-ctx.Done():
         fmt.Println(ctx.Err())
         return
      default:
         fmt.Println("dl1 doing.... ", n)
         n++
         time.Sleep(2 * time.Second)
      }
   }
}
func main() {
   // 设置deadline为当前时间之后的5秒那个时刻
   d := time.Now().Add(5 * time.Second)
   ctx, cancel := context.WithDeadline(context.Background(), d)
   defer cancel()
   go dl1(ctx)
   go dl2(ctx)
   time.Sleep(10 * time.Second)
}

图片.png

WithTimeout

实际就是调用了WithDeadline()

WithValue
package main

import (
   "context"
   "fmt"
   "time"
)

func v2(ctx context.Context) {
   fmt.Println(ctx.Value("key"))
   fmt.Println(ctx.Value("v1"))
   // 相同键,值覆盖
   ctx = context.WithValue(ctx, "key", "modify from v2")
}
func v1(ctx context.Context) {
   if v := ctx.Value("key"); v != nil {
      fmt.Println("key = ", v)
   }
   ctx = context.WithValue(ctx, "v1", "value of v1 func")
   go v2(ctx)
}
func main() {
   ctx, cancel := context.WithCancel(context.Background())
   // 向context中传递值
   ctx = context.WithValue(ctx, "key", "main**")**
   go v1(ctx)
   time.Sleep(10 * time.Second)
   cancel()
   time.Sleep(3 * time.Second)
}

图片.png

原理解析

1、通过 Background() 和 TODO() 创建最 emptyCtx 实例 ,通常是作为根节点

2、通过 WithCancel() 创建 cancelCtx 实例

3、通过 WithValue() 创建 valueCtx 实例

4、通过 WithDeadline 和 WithTimeout 创建 timerCtx 实例

1,Background()

Background(), Empty() 均会返回一个空的 Context emptyCtx。emptyCtx 对象在方法 Deadline(), Done(), Err(), Value(interface{}) 中均会返回nil,String() 方法会返回对应的字符串。这个实现比较简单

2,WithCancel 构造的context

type cancelCtx struct { 
Context 
// 互斥锁,保证context协程安全 
mu sync.Mutex 
// cancel 的时候,close 这个chan 
done chan struct{}
// 派生的context 
children map[canceler]struct{} 

err error 
}

WithCancel 方法首先会基于 parent 构建一个新的 Context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  c := newCancelCtx(parent)  // 新的上下文
  propagateCancel(parent, &c) // 挂到parent 上
  return &c, func() { c.cancel(true, Canceled) }
}

// 把child 挂在到parent 下
func propagateCancel(parent Context, child canceler) {
  // 如果parent 为空,则直接返回
  if parent.Done() == nil {
    return // parent is never canceled
  }
  
  // 获取parent类型
  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 {
    // 启动goroutine,等待parent/child Done
    go func() {
      select {
      case <-parent.Done():
        child.cancel(false, parent.Err())
      case <-child.Done():
      }
    }()
  }
}

其中,propagateCancel 方法会判断 parent 是否已经取消,如果取消,则直接调用方法取消;如果没有取消,会在parent的children 追加一个child。这里就可以看出,context 树状结构的实现。

Done()

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
}

实现比较简单,就是返回一个channel,等待chan 关闭。可以看出 Done 操作是在调用时才会构造 chan done,done 变量是延时初始化的。

在手动取消 Context 时

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  // 一些判断,关闭 ctx.done chan
  // ...
  if c.done == nil {
    c.done = closedchan
  } else {
    close(c.done)
  }

  // 广播到所有的child,需要cancel goroutine 了
  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()

  // 然后从父context 中,删除当前的context
  if removeFromParent {
    removeChild(c.Context, c)
  }
}

会调用 cancelCtx 的 cancel 方法, 这里可以看到,当执行cancel时,除了会关闭当前的cancel外,还做了两件事,

① 所有的child 都调用cancel方法,

② 由于该上下文已经关闭,需要从父上下文中移除当前的上下文。

3,4,定时取消功能的上下文

WithDeadline, WithTimeout 提供了实现定时功能的 Context 方法,返回一个timerCtx结构体。WithDeadline 是给定了执行截至时间,WithTimeout 是倒计时时间

WithTImeout 是基于WithDeadline实现的,因此我们仅看其中的WithDeadline即可

WithDeadline 内部实现是基于cancelCtx 的。相对于 cancelCtx 增加了一个计时器,并记录了 Deadline 时间点。下面是timerCtx 结构体:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   // 若父上下文结束时间早于child,
   // 则child直接挂载在parent上下文下即可
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      return WithCancel(parent)
   }

   // 创建个timerCtx, 设置deadline
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }

   // 将context挂在parent 之下
   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 {
      // 设定一个计时器,到时调用cancel
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

WithValue 构造的上下文与上面几种有区别,其构造的context 原型如下:

type valueCtx struct { // 保留了父节点的context 
    Context
    key, val interface{}
}

每个context

包含了一个Key-Value组合。

valueCtx 保留了父节点的Context,

但没有像cancelCtx 一样保留子节点的Context

func (c *valueCtx) Value(key interface{}) interface{} {
  if c.key == key {
    return c.val
  }
  // 从父context 中获取
  return c.Context.Value(key)
}

Value 的获取是采用链式获取的方法。如果当前 Context 中找不到,则从父Context中获取。如果我们希望一个context 多放几条数据时,可以保存一个map 数据到 context 中。这里不建议多次构造context来存放数据。毕竟取数据的成本也是比较高的。