源码面前,了无秘密。本文做为context分析系列的第二篇,会从源码的角度来分析context如何实现所承诺的功能及内在特性。本篇主要从如下四个角度阐述: context中的接口、context有哪些类型、context的传递实现、context的层级取消触发实现。安全
上一篇go context剖析之使用技巧中能够看到context包自己包含了数个导出函数,包括WithValue、WithTimeout等,不管是最初构造context仍是传导context,最核心的接口类型都是context.Context,任何一种context也都实现了该接口,包括value context。bash
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
复制代码
既然context都须要实现Context,那么包括不直接可见(非导出)的结构体,一共有几种context呢?答案是4种。并发
emptyCtx定义以下函数
type emptyCtx int
复制代码
为了减轻gc压力,emptyCtx实际上是一个int,而且以do nothing的方式实现了Context接口,还记得context包里面有两个初始化context的函数post
func Background() Context
func TODO() Context
复制代码
这两个函数返回的实现类型即为emptyCtx,而在contex包中实现了两个emptyCtx类型的全局变量: background、todo,其定义以下ui
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
复制代码
上述两个函数依次对应这两个全局变量。到这里咱们能够很肯定地说context的根节点就是一个int全局变量,而且Background()和TODO()是同样的。因此千万不要用nil做为context,而且从易于理解的角度出发,未考虑清楚是否传递、如何传递context时用TODO,其余状况都用Background(),如请求入口初始化contextspa
cancelCtx的cancel机制是手工取消、超时取消的内部实现,其定义以下设计
type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
复制代码
这里的mu是context并发安全的关键、done是通知的关键、children存储结构是内部最经常使用传导context的方式。3d
timerCtx内部包含了cancelCtx,而后经过定时器,实现了到时取消的功能,定义以下code
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
复制代码
这里deadline只作记录、String()等边缘功能,timer才是关键。
valueCtx是四个类型的最后一个,只用来传值,固然也能够传递,全部context均可以传递,定义以下
type valueCtx struct {
Context
key, val interface{}
}
复制代码
因为有的人认为context应该只用来传值、有的人认为context的cancel机制才是核心,因此对于valueCtx也在下面作了一个单独的介绍,你们能够经过把握内部实现后按照本身的业务场景作一个取舍(传值能够用一个全局结构体、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都具备传递性,而传递性的内在机制能够理解为: 在调用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的取消机制(组合取消的场景,按照先到先得的原则,不管那种条件触发的,都会传递调用cancel)。这里有两个设计很关键:
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)
}
}
复制代码
WithTimeout是经过WithDeadline来实现的,均对应timerCtx类型。经过parentCancelCtx函数的定义咱们知道,timerCtx也会记录父子context关系。可是timerCtx是经过timer定时器触发cancel调用的,部分实现以下
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
复制代码
这里暂时只想到了静默包含即type A struct{context.Context}的状况。经过parentCancelCtx和propagateCancel咱们知道这种context不会创建父子context的直接联系,可是会经过单独的goroutine去检测done channel,来肯定是否须要触发链路上的cancel函数,实现见propagateCancel的else部分。
context的实现并不复杂,可是在实际开发中确能带来不小的便利性。篇一力求你们可以按场景对号入座熟练地使用context,篇二但愿你们可以从源码层面了解到context的实现,在一些极端场景下,如静默包含context,也能从容权衡利弊,作到知其然知其因此然,谢谢。