H5W3
当前位置:H5W3 > 其他技术问题 > 正文

context 源码完全解析

基本的类型

首先来看的是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组合,形成复杂的操作,这就是其厉害之处。

本文地址:H5W3 » context 源码完全解析

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址