内容简介:源码面前,了无秘密。本文作为context分析系列的第二篇,会从源码的角度来分析context如何实现所承诺的功能及内在特性。本篇主要从以下四个角度阐述: context中的接口、context有哪些类型、context的传递实现、context的层级取消触发实现。上一篇既然context都需要实现Context,那么包括不直接可见(非导出)的结构体,一共有几种context呢?答案是
源码面前,了无秘密。本文作为context分析系列的第二篇,会从源码的角度来分析context如何实现所承诺的功能及内在特性。本篇主要从以下四个角度阐述: context中的接口、context有哪些类型、context的传递实现、context的层级取消触发实现。
context中的接口
上一篇 go context剖析之使用技巧 中可以看到context包本身包含了数个导出函数,包括WithValue、WithTimeout等,无论是最初构造context还是传导context,最核心的接口类型都是context.Context,任何一种context也都实现了该接口,包括value context。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
复制代码
到底有几种context?
既然context都需要实现Context,那么包括不直接可见(非导出)的结构体,一共有几种context呢?答案是 4种 。
- 类型一: emptyCtx,context之源头
emptyCtx定义如下
type emptyCtx int 复制代码
为了减轻gc压力,emptyCtx其实是一个int,并且以do nothing的方式实现了Context接口,还记得context包里面有两个初始化context的函数
func Background() Context func TODO() Context 复制代码
这两个函数返回的实现类型即为emptyCtx,而在contex包中实现了两个emptyCtx类型的全局变量: background、todo,其定义如下
var ( background = new(emptyCtx) todo = new(emptyCtx) ) 复制代码
上述两个函数依次对应这两个全局变量。到这里我们可以很确定地说context的根节点就是一个int全局变量,并且Background()和TODO()是一样的。 所以千万不要用nil作为context,并且从易于理解的角度出发,未考虑清楚是否传递、如何传递context时用TODO,其他情况都用Background(),如请求入口初始化context
- 类型二: cancelCtx,cancel机制之灵魂
cancelCtx的cancel机制是手工取消、超时取消的内部实现,其定义如下
type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
复制代码
这里的mu是context并发安全的关键、done是通知的关键、children存储结构是内部最常用传导context的方式。
- 类型三: timerCtx,cancel机制的场景补充
timerCtx内部包含了cancelCtx,然后通过定时器,实现了到时取消的功能,定义如下
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
复制代码
这里deadline只做记录、String()等边缘功能,timer才是关键。
- 类型四: valueCtx,传值
valueCtx是四个类型的最后一个,只用来传值,当然也可以传递,所有context都可以传递,定义如下
type valueCtx struct {
Context
key, val interface{}
}
复制代码
由于有的人认为context应该只用来传值、有的人认为context的cancel机制才是核心,所以对于valueCtx也在下面做了一个单独的介绍,大家可以通过把握内部实现后按照自己的业务场景做一个取舍(传值可以用一个全局结构体、map之类)。
value context的底层是map吗?
在上面valueCtx的定义中,我们可以看出其实value context底层不是一个map,而是每一个单独的kv映射都对应一个valueCtx,当传递多个值时就要构造多个ctx。同时,这要是value contex不能自低向上传递值的原因。
valueCtx的key、val都是接口类型,在调用WithValue的时候,内部会首先通过反射确定key是否可比较类型(同map中的key),然后赋值key
在调用Value的时候,内部会首先在本context查找对应的key,如果没有找到会在parent context中递归寻找,这也是value可以自顶向下传值的原因。
context是如何传递的
首先可以明确,任何一种context都具有传递性,而传递性的内在机制可以理解为: 在调用WithCancel、WithTimeout、WithValue时如何处理父子context 。从传递性的角度来说,几种With*函数内部都是通过propagateCancel这个函数来实现的,下面以WithCancel函数为例
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
复制代码
newCancelCtx是cancelCtx赋值父context的过程,而propagateCancel建立父子context之间的联系。
propagateCance定义如下
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {// context包内部可以直接识别、处理的类型
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 {// context包内部不能直接处理的类型,比如type A struct{context.Context},这种静默包含的方式
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
复制代码
1.如果parent.Done是nil,则不做任何处理,因为parent context永远不会取消,比如TODO()、Background()、WithValue等。 2.parentCancelCtx根据parent context的类型,返回bool型ok,ok为真时需要建立parent对应的children,并保存parent->child映射关系(cancelCtx、timerCtx这两种类型会建立,valueCtx类型会一直向上寻找,而循环往上找是因为cancel是必须的,然后找一种最合理的。),这里children的key是canceler接口,并不能处理所有的外部类型,所以会有else,示例见上述代码注释处。对于其他外部类型,不建立直接的传递关系。 parentCancelCtx定义如下
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
}
}
}
复制代码
context是如何触发取消的
上文在阐述传递性时的实现时,也包含了一部分取消机制的代码,这里不会再列出源码,但是会依据上述源码进行说明。对于几种context,传递过程大同小异,但是取消机制有所不同,针对每种类型,我会一一解释。不同类型的context可以在一条链路进行取消,但是每一个context的取消只会被一种条件触发,所以下面会单独介绍下每一种context的取消机制(组合取消的场景,按照先到先得的原则,无论那种条件触发的,都会传递调用cancel)。这里有两个设计很关键:
- cancel函数是幂等的,可以被多次调用。
- context中包含done channel可以用来确认是否取消、通知取消。
- cancelCtx类型
cancelCtx会主动进行取消,在自顶向下取消的过程中,会遍历children context,然后依次主动取消。 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)
}
}
复制代码
- timerCtx类型
WithTimeout是通过WithDeadline来实现的,均对应timerCtx类型。通过parentCancelCtx函数的定义我们知道,timerCtx也会记录父子context关系。但是timerCtx是通过timer定时器触发cancel调用的,部分实现如下
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
复制代码
- 静默包含context
这里暂时只想到了静默包含即type A struct{context.Context}的情况。通过parentCancelCtx和propagateCancel我们知道这种context不会建立父子context的直接联系,但是会通过单独的goroutine去检测done channel,来确定是否需要触发链路上的cancel函数,实现见propagateCancel的else部分。
结尾
context的实现并不复杂,但是在实际开发中确能带来不小的便利性。篇一力求大家能够按场景对号入座熟练地使用context,篇二希望大家能够从源码层面了解到context的实现,在一些极端场景下,如静默包含context,也能从容权衡利弊,做到知其然知其所以然,谢谢。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【Java集合源码剖析】ArrayList源码剖析
- Java集合源码剖析:TreeMap源码剖析
- 我的源码阅读之路:redux源码剖析
- ThreadLocal源码深度剖析
- SharedPreferences源码剖析
- Volley源码剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning Apache Struts
Arnold Doray / Apress / 2006-02-20 / USD 44.99
Beginning Apache Struts will provide you a working knowledge of Apache Struts 1.2. This book is ideal for you Java programmers who have some JSP familiarity, but little or no prior experience with Ser......一起来看看 《Beginning Apache Struts》 这本书的介绍吧!