【Golang】源码学习:contex包——从功能到源码理解Go Context机制(二)

第一部分链接:https://blog.csdn.net/qq_38093301/article/details/104370248

三、源码学习

Context包高度提炼了功能边界,对外只暴露统一的Context接口,最大程度的隐藏的实现细节,对于接口的设计非常简洁。

Context接口的定义如下:

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

这四个函数对外提供的功能不再赘述,在context包中,共有四个类型实现了该接口:

emptyCtx:默认初始的context使用的类型,仅实现Context接口,不做任何处理,返回默认空值。

valueCtx:对应携带K-V值的context接口实现,携带k-v数据成员,实现了value函数的具体操作。

cancelCtx:实现了cancler接口,为context提供了可取消自身和子孙的功能。

timerCtx:在cancelCtx的基础上,对带有定时功能的Context进行了实现。

1、emptyCtx:即空context,也是所有子context的祖先

emptyCtx的定义非常简单,但是整个Context包中最精彩的设计之一:

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"
}

可以看到,emptyCtx仅对int类型做了新的类型定义,然后实现了Context接口,各函数不做任何操作,仅仅返回默认值。

这个实现只用于在包内定义两个内部实例,并提供对外访问函数。

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

func Background() Context {
	return background
}
func TODO() Context {
	return todo
}

在外部使用context.Background()和context.Background()操作,直接返回两个定义好的emptyCtx类型的实例,该实例中即不包含任何信息,实现的函数也不做任何操作,是一个不包含任何状态的上下文,在设计上正符合一个默认/初始context的逻辑。

而将emptyCtx实例作为所有context的祖先,在代码设计上还有更多的优点。在后面的代码中可以看到,子context为了继承父context包含的状态,将把父context作为一个匿名成员内嵌(Embedding)在自身结构中,形成一个包含链条,将一个无任何实现的空状态作为初始包含的context,是一个很巧妙的设计:

1、不需要再对父Context是否为空作为额外的判断,优化了代码结构,在调用时逻辑也更通顺。

2、Go语言不支持继承,而内嵌一个匿名成员,实际上达到了继承的效果,在后面可以看到,因为以一个完全实现了context接口的emptyCtx实例为起点,cancelCtx等实现已经继承了默认的函数,只需要再实现需要用到的函数即可,缺失的其他函数一定会被最底层的emptyCtx实例提供。

2、valueCtx:在原状态基础上添加一个键值对

type valueCtx struct {
	Context
	key, val interface{}
}

valueCtx在原有状态基础上添加了一个空接口类型的键值对信息,可以存储所有可判等的类型,由WithValue函数建立,该函数在判断类型合法后,使用父context和k-v键值对建立了valueCtx实例并返回。

func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

valueCtx类型真正实现了value函数,该函数是一个向上递归的查询过程,如果key不存在,将递归调用emptyCtx定义好的默认函数,返回一个nil值。

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

3、cancelCtx:建立可主动关闭的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
}

在cancelCtx的结构定义中,包含了互斥锁用于保证Context的线程安全,通道实例done向本runtion外发送本context已经被关闭的的消息,err字段用于标记该context是否已经被取消,取消则将是非空值,children字典则存储了本context派生的所有context,key值为canceler类型,因为可以到设计者归纳出了一个canceler接口包含context关闭的操作,而cancelCtx正实现了这一接口。

type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}

在使用newCancelCtx存储好父context后,WithCancel将调用propagateCancel,完成处理cancel操作需要的逻辑。

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

 propagateCancel完成的主要工作为,将新建立的cancelCtx,绑定到祖先的取消广播树中,简单来说,就是将自身存储到最近的*cancelCtx类型祖先的chindren列表中,接收该祖先的广播。

1、parent.Done()==nil,祖先为不可取消类型,则自己就是取消链的根,直接返回。

2、通过辅助函数parentCancelCtx向上回溯,尝试找到最近的*cancelCtx类型祖先。

3、如果成功找到,则先判断是否已经被取消,如果为否,则将自身加入其children列表中。

4、否则,就单独监听其父context和自己的取消情况。

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():
			}
		}()
	}
}

得益于子context引用父context的设计,对于每个contest都将可以通过向上回溯得到一条引用链,辅助函数 parentCancelCtx即通过不断向内部引用类型转换,达到回看context历史的目的,寻找最近的*cancelCtx型祖先。

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
		}
	}
}

 完成取消链的绑定工作后,返回的cancelFunc函数将调用该context的cancel函数完成取消上下文操作

 func() { c.cancel(true, Canceled) }

 cancel函数将本context自身关闭(close(c.done)),并将像其在取消链中的子节点广播取消通知,这一起了以该节点为根的下属取消链以泛洪的方式取消了所有节点,最后调用removeChild函数,接触自身在取消链中的绑定。

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.err判断是否已经被取消了
		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)
	}
}

3、timerCtx:带有计时器的可取消context

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.
	deadline time.Time
}

与前两种实现不同,timerCtx实现中明确要引用一个cancelCtx结构,因为其本质是在一个cancelCtx基础上添加了一个计时器功能,建立函数WithDeadline如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	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 {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

可以看到,出去一些与输入deadline相关的判断逻辑外,该函数主要完成了两项工作:

  • 使用父Context建立一个cancelCtx
  • 计算预设时间,使用time.AfterFunc函数,建立一个计数器,到时后,主动调用cancel函数取消该context

因此本质上说,无论是WithDeadline还是WithTimeout生成的子context,都是对于time.AfterFunc函数和cancelCtx的一个封装。

四、学习总结

1、在context包的设计结构中存在两大重要结构

  • 从内向外的包含链,在构造子Context时包含其父context的实例而形成,这一链的最深处将是一个初始的emptyCtx实例,提供了默认的方法,该链通过四个With方法建立新的子context而不断延申,这一链条使得子节点包含了全部祖先节点的信息,即当前的上下文状态必定包含了历史的上下文状态,从底层实现上看,通过.(Type)方法,子节点可以还原为任意祖先节点的状态,从而做出修改和判断,通过向内回溯的方式,确定是否在某一节点通过WithValue函数存储了要找的k-v值,使得历史上下文存储的信息都能被叶子节点查阅。而第二条取消链的形成,也是通过在这条链上回溯而完成的。
  • 为通过WithCancel函数建立的取消链,实际上是由多条链组成的树形结构,在一个cancelCtx节点的children字典中包含了其下属的所有子cancelContext,因此递归地形成了一颗取消树,当一个cancelCxt建立时,将寻找其父cancelCxt并把自身加入到children列表中,当对一个节点调用cancel函数时,将向其包含的所有子cancelCxt广播取消消息,从而形成泛洪式广播。

2、虽然所有的内部结构都被高度归纳为了一个context接口,仍可以对 其进行二次分类,在context源码中,定义了一个内部接口canceler,只有实现了该接口的实现才被看作是一个可取消context,这是一个多重实现设计。

3、使用interface和Embedding(内嵌匿名字段),可以完整地还原出继承+多态的oop设计,甚至感觉更加灵活。

4、context包在最大程度上隐藏了内部细节,对外提供了简洁的context接口,是接口设计的优秀范例

发布了34 篇原创文章 · 获赞 1 · 访问量 1711

猜你喜欢

转载自blog.csdn.net/qq_38093301/article/details/104378837
今日推荐