前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言上下文Context包源码分析和实践

Go语言上下文Context包源码分析和实践

作者头像
阿伟
发布2019-12-17 17:29:25
8420
发布2019-12-17 17:29:25
举报
文章被收录于专栏:GoLang那点事GoLang那点事
context包来源和作用

context包最早在golang.org/x/net/context中,在Go1.7时,正式被官方收入,进入标准库,目前路径为src/context/,目前context包已经在Go各个项目中被广泛使用。并且在Co中Context和并发编程有着密切的关系(context ,chan ,select,go这些个词经常密不可分)

其主要功能我列举如下:

  1. 跨服务,方法,进程的key,value传递
  2. 跨服务,方法,进程的超时控制
  3. 跨服务,方法,进程的取消执行

其主要的应用场景也非常多,我列举如下几个

  1. 全链路服务,日志追踪,记录
  2. 客户端,服务端方法调用超时控制
  3. 跨进程间延迟,取消信号,截至时间

在一些常见的Web服务中,比如Go自身携带的Http服务器中,客户端每发生一个请求,服务端都会开一个goroutine,在开启的这个goroutine中又会开启多goroutine处理不同的逻辑,Context就是把所有的goroutine串联起来,使之有一个统一的上下文,通过这么一个上下文,从而达到在多个goroutine之间携带Key,Value,资源控制,超时处理。

Context的定义
代码语言:javascript
复制
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
type canceler interface {
   cancel(removeFromParent bool, err error)
   Done() <-chan struct{}
}

Context是一个接口,定义了四个方法,这四个方法作用如下:

  1. Deadline,第一个出参 获取设置的截至时间点,第二个参数表示是否设置截至时间点
  2. Done,返回一个通道,如果这个通道可读,代表Context已经发起了取消,说白了这是一个信号传递,当从Done中能够读取出数据时,就该做一些工作了
  3. Err,返回Context取消的原因
  4. Value(key),返回Context的绑定的值,就是根据Key获取Value

canceler也是接口,这是一个取消器,定义了两个方法,作用如下:

  1. cancel(),取消Context,参数支持传入是否取消父Context,以及取消的原因
  2. Done 返回一个通道,如果这个通道可读,代表Context已经发起了取消。
Context的实现

Context接口的实现总共有四种(emptyCtx,timerCtx,cancelCtx,valueCtx),先从结构体定义看一下这几个的关系,这四个实现结构体彼此通过组合的关系可以实现功能的复用。timerCtx中包含了cancelCtx,所以timerCtx具备了取消的功能,cancelCtx以及valueCtx包含了Context,这个Context一般是根Context(也就是emptyCtx)。

代码语言:javascript
复制
type emptyCtx int
type timerCtx struct {
   cancelCtx //一个取消的cancelCtx(包含了cancelCtx)
   timer *time.Timer //计时的timer
   deadline time.Time //超时时间
}
type cancelCtx struct {
   Context  //包含了空的Context,这个一般是入参传入的emptyCtx
   mu       sync.Mutex            // 同步锁
   done     chan struct{}         //
   children map[canceler]struct{} // 子context的取消器
   err      error                 // 第一个取消器取消的原因
}
type valueCtx struct {
//包含了空的Context,这个一般是入参传入的emptyCtx,作为根Context   
   Context 
//存储kv的结构,就是两个变量,不是想象中的map,因为只能存储一堆kv。
   key, val interface{} ,
}
emptyCtx分析

一个emptyCtx,什么也不做,没有取消,没有超时,没有Value,为什么这个是int类型而不是struct?因为struct{}是一个空的指针,所有struct{}的地址都是一样的,但emptyCtx类型的变量需要有确切的地址,所有采用了int类型,emptyCtx实现的四个方法都是空的方法体,同时还有两个通过emptyCtx创建的全局Context,如下代码,backgroud一般是作为根的Context,用来派生其它子Context,todo比较好理解,就是你也没想好用来做什么,但总觉得需要一个context,就先用todo代理,后期再处理。

代码语言:javascript
复制
var (        
    background = new(emptyCtx)       
    todo       = new(emptyCtx)
)

如下代码,就context包中提供的两个方法,用来返回background,todo

代码语言:javascript
复制
func Background() Context {
   return background
}
func TODO() Context {
   return todo
}
cancelCtx分析

cancelCtx的创建方法主要是WithCancel(),这个方法创建出一个子的上下文,并返回一个能够取消该上下文的函数,主要是方便开发人员手动在未来想要的某一时刻执行取消,从而达到信号通知,让其它协程能够给做一些例如资源清理或者中断的工作。

代码语言:javascript
复制
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   c := newCancelCtx(parent)
   //检测父Context是否执行取消,如果执行,则该父Context的的所有子Context也执行cancel()方法,负责,开启协程等待父Context执行cancel
   propagateCancel(parent, &c)
   //返回子上下文,和取消函数
   return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent}
}

我们看cancelCtx实现的Done方法,这个方法是cancelCtx和timerCtx通用的,因为timerCtx结构体中包含了cancelCtx,所以timerCtx就不需要再实现了,下文讲到的cancelCtx就只会实现Deadline方法了。

代码语言:javascript
复制
func (c *cancelCtx) Done() <-chan struct{} {
   c.mu.Lock()
   //创建一个chan通道,然后返回,很简单的
   if c.done == nil {
      c.done = make(chan struct{})
   }
   d := c.done
   c.mu.Unlock()
   return d
}
func (c *cancelCtx) Err() error {
   c.mu.Lock()
   //返回context取消的原因
   err := c.err
   c.mu.Unlock()
   return err
}

我们主要看看返回的cancel函数,这个函数非常重,因为在他里面实现了取消的逻辑,它是congtext能够取消的核心,核心代码如下

代码语言:javascript
复制
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   c.mu.Lock()
   // 只要err不为空,就代表一定取消了,因为只有取消了,才会有取消的原因
   if c.err != nil {
      c.mu.Unlock()
      return
   }
   c.err = err
   //关闭done()方法返回的通道
   if c.done == nil {
      c.done = closedchan
   } else {
     //取消context,关闭chan时,会返回一个空的结构体{}
      close(c.done)
   }
   //遍历congtext的子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()
   //是否从父context的把自己移除,这个一般是true,除非很明确没有父context
   if removeFromParent {
      removeChild(c.Context, c)
   }
}
timerCtx分析

timeCtx的创建有两种方法:WithTimeout和WithDeadline,其i中WithTimeout中单纯的调用了WithDeadline,并没有做其它处理,从名字大家或多或少可以猜出timeCtx的核心就是做超时,延迟。两个比较相似,但具体实现上略有不同,WithDeadline创建过程的代码如下:

代码语言:javascript
复制
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      // 当前的截止日期已经比新的截止日期早,则直接取消
      return WithCancel(parent)
   }
   //创建一个取消的cancelCtx
   c := &timerCtx{
      //从这可以看出timerCtx是包含了cancelCtx的功能的
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c) //传播cancel
   dur := time.Until(d)
   if dur <= 0 {
      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() {//开启计时,到时间点后回调func
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) } 
}

看完代码,核心其实比较简单,一判断父context是否也是deadline,时间是否在传入的时间之前,二判断父context是否已经取消,如果取消,则子的context也全部取消,两个条件判断完成之后,启动计时器,返回deadline和cancel,一个是用户主动调用cancel取消,一个是时间到达之后回调cancel取消。

我们再来看一看timerCtx实现的Deadline方法,其实很简单,就是返回到期的时间以及一个true标识符

代码语言:javascript
复制
//只有这个方法才是timerCtx特有实现的的方法
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
   return c.deadline, true
}

这个其实是emptyCtx的空实现,因为timerCtx本身没有存储key,value的设计

代码语言:javascript
复制
func (*emptyCtx) Value(key interface{}) interface{} {   
   return nil
}

我们再来看看timerCtx的取消方法是如何实现的,首先明确timerCtx有两种取消的办法,一个是手动取消,一个是计时器到时间了自动取消,调用的都是这个方法.

代码语言:javascript
复制
func (c *timerCtx) cancel(removeFromParent bool, err error) {
   //内部还是调用了cancelCtx的取消
   c.cancelCtx.cancel(false, err)
   if removeFromParent {
      // Remove this timerCtx from its parent cancelCtx's children.
      removeChild(c.cancelCtx.Context, c)
   }
   c.mu.Lock()
   //停止计时器
   if c.timer != nil {
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}
valueCtx分析

valueCtx的创建方法是WithValue(key,value interface{}),这个方法只会返回一个子的上下文,方法入参是一对kv,这个kv会被存储到这个子上下文中,其存储结构就是两个变量,可以看上面的valueCtx结构体,这个子上下文可以调用Value(key interface{})根据key获取value。

代码语言:javascript
复制
func WithValue(parent Context, key, val interface{}) Context {
   if key == nil {
      panic("nil key")
   }
   if !reflect.TypeOf(key).Comparable() {
      panic("key is not comparable")
   }
   return &valueCtx{parent, key, val}
}

通过context的Value方法可以根据key获取value,看看这个方法的实现,其实很简单

代码语言:javascript
复制
func (c *valueCtx) Value(key interface{}) interface{} {
    //如果key相等,返回v
   if c.key == key {
      return c.val
   }
   //递归调用,直到最后调用到emptyCtx,找不到返回nil
   return c.Context.Value(key)
}
Context包Demo实践

我们通过4个实践context各种功能,以便大家能够理解 context的超时

代码语言:javascript
复制
func ContextWithTimeOut() {
   ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
   //开启新的协程
   go func(ctx context.Context) {
      //模拟处理任务
      time.Sleep(10 * time.Second)
      fmt.Println("任务处理完成")
   }(ctx)
   select {
   case <-ctx.Done():
      fmt.Println("context timeout")
   }
}

context的取消

代码语言:javascript
复制
func ContextWithCancel() {
   wg := sync.WaitGroup{}
   wg.Add(1)
   ctx, cancel := context.WithCancel(context.Background())
   go func(ctx context.Context) {
      select {
      case c := <-ctx.Done():
         {
            fmt.Println("context被手动取消了")
            wg.Done()
         }
      }
   }(ctx)
   //手动取消
   cancel()
   wg.Wait()
}

context获取key value

代码语言:javascript
复制
//上下文key value,根据key获取,
//当子context 获取不到对应key时,就取父的context获取
func ContextWithValue() {
   wg := sync.WaitGroup{}
   wg.Add(1)
   ctx := context.WithValue(nil, "key", "value")
   go func(ctx context.Context) {
      ctxB := context.WithValue(ctx, "key1", "value1")
      v := ctx.Value("key")
      v1 := ctxB.Value("key1")
      v2 := ctxB.Value("key") //这个时候会获取ctxA的key
      fmt.Println(v, v1, v2)
      wg.Done()
   }(ctx)
   wg.Wait()
}

context的延迟取消

代码语言:javascript
复制
//可延迟一定的时间取消,也可以手动编码取消
func ContextWithDeadLine() {
   wg := &sync.WaitGroup{}
   wg.Add(1)
   ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3)) 
   go func(wg *sync.WaitGroup, ctx context.Context) {
      select {
      case <-ctx.Done():
         fmt.Println("3秒延迟后取消,打印ctx deadline done")
         wg.Done()
      }
   }(wg, ctx)
   wg.Wait()
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GoLang那点事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • context包来源和作用
  • Context的定义
  • Context的实现
    • emptyCtx分析
      • cancelCtx分析
        • timerCtx分析
          • valueCtx分析
          • Context包Demo实践
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档