context 源码完全解析

胡大海 · · 345 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

基本的类型

首先来看的是Context到底是什么?源码中的定义是一个接口,有四个方法。

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

四个方法中除了Err()是一个通用的方法外,其他三个方法都各对应一种Context类型。也就是说Context的实际结构体类型主要有三种(后面会对为什么说主要做解释)。

通常,在初始化一个Context的时候,我们都会使用一个方法来创建一个初始化的参数context.Background()

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
   return
}

func (*emptyCtx) Done() <-chan struct{} {
   return nil
}

func (*emptyCtx) Err() error {
   return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
   return nil
}

func (e *emptyCtx) String() string {
   switch e {
   case background:
      return "context.Background"
   case todo:
      return "context.TODO"
   }
   return "unknown empty Context"
}

var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)

func Background() Context {
   return background
}

func TODO() Context {
   return todo
}
复制代码

可以看出来,background其实是一个*emptyCtx,并且实现了Context的四个方法。文档中对Background方法的注释就说了: 这个Context不会被取消,没有值,也没有截止时间。通常的使用方式有如下四种

  1. main函数
  2. Context初始化的时候
  3. 测试用例
  4. 作为处理请求的顶层的Context

在服务处理一个请求的时候,各种操作依照依赖的顺序和执行的顺序可以组成一个树状的结构,由根节点像外扩散。为了做好整体的控制,在超时或者某些条件下,后续的操作就不用执行了,这个就需要用户自己实现。而各种情况下的自己实现,是比较耗费时间以及精力的。于是context包就诞生了。

其实context就是为了在树状的结构中,控制请求在没有必要的时候不再执行。也就是说,到了没有必要的时候,我就需要让树状的结构中,某个子树下的所有操作都取消。所以取消操作是Context的根本操作。取消操作的结构体定义如下:

type cancelCtx struct {
   Context

   mu       sync.Mutex            // protects following fields
   done     chan struct{}         // created lazily, closed by first cancel call
   children map[canceler]struct{} // set to nil by the first cancel call
   err      error                 // set to non-nil by the first cancel call
}
复制代码

这个里面的Context是比较有意思的,可以是四种Context中的任何一个,还可以是emptyCtx,然后就可以任意的组合。其中的done表示是否已经取消,children表示此Context下的子Context,从而控制子树下所有的操作都取消。

操作流程

初始化

初始化一个cancelCtx的步骤通常如下

ctx, cancel := context.WithCancel(context.Background())
复制代码

通过查看源码,可以看到WithCancel的操作如下

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   c := newCancelCtx(parent)
   propagateCancel(parent, &c)
   return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	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 {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
复制代码

newCancelCtx比较简单,初始化了一个cancelCtx。复杂点的操作是propagateCancel,这个操作就和原文的注释一样了,为了在父Context取消时候,此Context也可以进行取消的操作。而传入的parent可能并不是一个cancelCtx的类型,所以需要不断的往父节点寻找,直到找到一个cancelCtx类型的Context。这个过程需要调用方法parentCancelCtx,对其类型的判断有三种cancelCtxtimerCtx以及valueCtx。这个函数应该是不会返回false的,因为在调用此函数之前,判断了parent.Done() == nil,如果成立,则说明是background或者todo。如果不成立,则实现了Context的结构体类型只有这三种了。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   for {
      switch c := parent.(type) {
      case *cancelCtx:
         return c, true
      case *timerCtx:
         return &c.cancelCtx, true
      case *valueCtx:
         parent = c.Context
      default:
         return nil, false
      }
   }
}
复制代码

个人认为,propagateCancel函数中p, ok := parentCancelCtx(parent)中的ok不会是false的,并且parentCancelCtx的返回值也不会是false的。至于这种此时不可能到达的代码,我猜测是为了使得判断的最为完备。

取消

WithCancel还返回了一个cancel的函数,这个函数的作用是什么呢?

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
   if c.done == nil {
      c.done = closedchan
   } else {
      close(c.done)
   }
   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 {
      removeChild(c.Context, c)
   }
}
func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

复制代码

这个操作中的第一个参数removeFromParent比较有意思,表示此Context节点是否应该从父节点的子节点中删除。照理说此节点的所有子节点都应该从父节点中删除,但是没有,只有在第一次调用cancel函数的时候,才会传入参数removeFromParenttrue,其他的时候都是false。其实仔细想想也就不难理解了,没有必要。这个节点可能有多个子节点,并且子节点也可能有很多子节点,这些节点从不从父节点删除都是无所谓的,因为一颗子树已经删除了,后续的每个节点的剥离只不过是浪费时间。

由于这种删除操作是深度优先的,如果都传入true,则会从最底部的节点开始删除。并不会因为传入true,就会造成删除过程出现 bug。

其他类型

调用方法有四种,分别如下

  1. WithCancel
  2. WithDeadline
  3. WithTimeout
  4. WithValue

WithDeadline返回的是timerCtx类型,就是包了一层的cancelCtx。可以定时到指定的时间执行cancel的操作,或者手动的执行cancel操作。

WithTimeout是转化为WithDeadline执行的。

WithValue大家可以在网上找找例子看看如何使用,其返回的类型为valueCtx也没什么说的,各位看看代码就可以理解了。

总结

个人感觉这个包的源码挺简单的,但是解决的问题是非常具有重要意义的。这个包虽然简单,但是可以通过各种Context组合,形成复杂的操作,这就是其厉害之处。


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:胡大海

查看原文:context 源码完全解析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

345 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传