内容简介:源码面前,了无秘密。本文作为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源码剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。