1.语境介绍
很多时候,我们会遇到这样的情况,上、下go routine需要同时取消,这就涉及到go routine之间的通信。在围棋中,建议我们通过交流来共享内存,而不是通过共享内存。
因此,我们需要使用channl。但是,在上面的场景中,如果需要我们自己处理channl的业务逻辑,就会有大量费时费力的重复性工作,于是就出现了context。
上下文是Go中进程通信的一种方式,它的底层是借助channl和snyc.Mutex来实现的。
2.基本介绍
context的底层设计可以概括为一个接口、四种实现和六种方法。
1个接口
上下文指定了上下文的四种基本方法。
四种实现
EmptyCtx实现了一个空上下文,可以用作根节点。
CancelCtx实现了一个带有取消功能的上下文,可以主动取消。
TimerCtx实现了一个由定时器timer和deadline取消的上下文。
ValueCtx实现了一个可以通过key和val字段存储数据的上下文。
六种方法
后台返回一个emptyCtx作为根节点。
TODO将emptyCtx作为未知节点返回。
WithCancel返回一个cancelCtx。
WithDeadline返回一个timerCtx。
WithTimeout返回一个timerCtx。
WithValue返回一个valueCtx。
3.源代码分析
3.1上下文接口
类型上下文接口{ Deadline()(截止时间。Time,ok bool) Done() -chan struct{} Err()错误值(键接口{})接口{}}
Deadline():返回一个时间。时间,表示当前上下文应该结束的时间,ok表示有结束时间。
Done():返回一个只读的chan。如果可以从该chan中读取数据,则ctx已被取消。
Err():返回取消上下文的原因。
Value(key):返回与密钥相对应的值,该值是安全的。
3.2 emptyCtx
键入emptyCtx int func(* emptyCtx)Deadline()(截止时间。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 }
EmptyCtx实现了一个空的上下文接口,它的主要功能是为两个方法返回预先初始化的私有变量Background和TODO,这些变量将在同一个Go程序中重用:
var(Background=new(emptyCtx)TODO=new(emptyCtx))func Background()Context { return Background } func TODO()Context { return TODO }
背景和TODO在实现上没有区别,但在用法语义上有区别:
背景是上下文的根节点;
TODO只应在您不确定应该使用哪个上下文时使用;
3.3取消tx
CancelCtx实现了取消器接口和上下文接口:
类型取消器接口{ cancel(removefrompertanchor bool,err error) Done() -chan struct{}}
其结构如下:
typecancetxstruct {//直接嵌入上下文。那么你可以把cancelCtx看成一个context上下文musync。互斥//保护下列原子域。chan struct {}的值//延迟创建,由第一次取消调用关闭子映射[canceler]struct{} //由第一次取消调用设置为空err error //由第一次取消调用设置为非空}
我们可以使用WithCancel方法来创建cancelCtx:
func with cancel(parent Context)(CTX Context,cancel cancel func){ if parent==nil { panic('无法从nil parent ')} c:=newCancelCtx(parent)propagate cancel(parent,c) return c,func() { c.cancel(true,cancel)} } func newCancelCtx(parent Context)cancelCtx { return cancelCtx { Context:parent } }
在上面的方法中,我们传入一个父上下文(通常是一个背景作为根节点),返回新创建的上下文,并以闭包的形式返回一个cancel方法。
NewCancelCtx将传入的上下文包装到一个私有结构context.cancelCtx中。
PropagateCancel将建立父子上下文之间的关联,并形成一个树形结构。当父上下文被取消时,子上下文也将被取消:
Func propagate cancel(父上下文,子取消器){//1。如果父ctx是不可撤销的ctx,那么直接返回done而不关联:=parent。done()if done==nil { return//parent永不取消}//2。然后判断父ctx是否已被取消选择{case-done://2.1如果父ctx已被取消,则无需关联。//那么这里应该顺便取消子ctx,因为父ctx已经取消了子ctx。//这是因为还没有关联,需要手动触发取消。//父级总是被取消的子级。取消(假,父。err())返回默认值:}//3。从父ctx中提取Cancelctx并将子ctx添加到父ctx的子系中if P,OK:=parentcanceltx(parent);Ok {p.mu.Lock() //仔细检查确认父ctx是否已经取消如果p.err!=nil {//直接取消当前子ctx父级已经取消子级。Cancel (false,P.err)} else {//否则,将其添加到children if p . children==nil { p . children=make(map[canceler]struct { })} p . children[child]=struct { } } p . mu . unlock()} else。一个新的信号原子。addint32 (goroutines,1) gofunc () {select {case-parent。done (): child。取消(假,父。err ()) case-child。done ():}}}
上述方法可能会遇到以下情况:
当父母。Done()==nil,即parent不会触发取消事件,当前函数直接返回;
当child的继承链中包含可取消的上下文时,会判断父代是否触发了取消信号;
如果已经取消,孩子将立即被取消;
如果不取消,孩子会被添加到家长的孩子列表中,等待家长释放取消信号;
当父上下文是开发人员定义的类型时,该上下文。实现上下文接口,并在Done()方法中返回非空管道;
运行一个新的Goroutine来监听双亲。Done()和child。Done()通道;
调用child.cancel取消子上下文。Done()已关闭;
PropagateCancel用于同步父节点和子节点之间的取消和结束信号,从而保证当父节点被取消时,子节点也会收到相应的信号,不会出现不一致的状态。
func parentCancelCtx(父上下文)(*cancelCtx,Bool) {done :=parent。Done() //如果Done为nil,则此ctx不可撤销//如果done==closedchan,则此ctx不是标准的cancelCtx。可能是用户自定义的if done==closed chan | | done==nil { return nil,false }//然后调用value方法从ctx中提取cancectxp,ok:=parent。值(cancectxkey)。(* cancectx)如果!Ok {return nil,false} //最后判断cancelCtx中存储的done是否与父级Ctx中的done一致//如果不一致,说明父级不是取消CTX P done,_:=p.done.load()。(chanstruct {}) if P done!=done { return nil,false } return p,true}
ancelCtx的done方法将返回一个更改的结构{}:
func(c * cancel CTX)Done()-chan struct { } { d:=c . Done . load()if d!=nil { return d .(chan struct { })} c .lock()defer c .unlock()d=c . done . load()if d==nil { d=make(chan struct { })c . done . store(d)} return d .(chan struct { })} var closed chan=make(chan struct { })
parentCancelCtx其实就是判断父上下文中是否有CancelCtx。如果有,它将被返回,这样子上下文就可以“链接”到父上下文。如果不是,将返回false,并打开一个新的goroutine进行监听。
3.4定时器rCtx
TimerCtx不仅通过嵌入cancelCtx接受相关的变量和方法,还通过持有timer和deadline实现了定时取消的功能:
键入timer CTX struct { cancel CTX timer * time。timer//Under cancel CTX截止时间。Time}func (c *timerCtx) Deadline()(截止时间。Time,ok bool) { return c.deadline,true } func(c * timerCtx)cancel(removefromparten bool,err error){ c . cancel tx . cancel(false,err)if removefromparten { remove child(c . cancel CTX . context,c)} c . mu lock()if c . timer!=nil { c . timer . stop()c . timer=nil } cunlock()}
3.5价值Ctx
ValueCtx还有两个字段key和val来存储数据:
type valueCtx struct {上下文键,val接口{}}
值搜索的过程实际上是一个递归搜索过程:
func(c * Value CTX)Value(key interface { })interface { } { if c . key==key { return c . val } return c . context . Value(key)}
如果键与当前ctx中存储的值一致,则直接返回,否则在parent中查找。最后找到根节点(一般是emptyCtx)直接返回一个nil。所以在使用Value方法时,需要判断结果是否为nil,类似于链表。效率很低,不建议传递参数。
使用建议
在官方博客中,提出了一些使用语境的建议:
不要把上下文塞进结构里。上下文类型直接作为函数的第一个参数,一般命名为ctx。
不要传递nil上下文:todo函数。如果你真的不知道要传递什么,标准库为你准备了一个上下文:todo。
不要把应该是函数参数的类型塞进上下文中。上下文应该存储一些公共数据。例如:登录会话、cookie等。
相同的上下文可以传递给多个goroutine。不用担心,上下文是并发的,安全的。
审计彭静