艾巴生活网

您现在的位置是:主页>科技 >内容

科技

context的四个基本方法

2023-11-05 09:17:01科技帅气的蚂蚁
1 背景介绍很多时候,我们会遇到上下go routine需要同时取消的情况,这就涉及到go routine之间的通信。在围棋中,建议我们通过通信共享内

context的四个基本方法

1.背景介绍

很多时候,我们会遇到上下go routine需要同时取消的情况,这就涉及到go routine之间的通信。在围棋中,建议我们通过通信共享内存,而不是共享内存。

因此,我们需要使用channl。但是,在上面的场景中,如果需要我们自己处理channl的业务逻辑,就会有大量费时费力的重复性工作。因此,出现了上下文。

它是一种在上下文中进行进程通信的方式,其底层借助channl和snyc来实现。互斥(体)…

2.基本介绍

context的底层设计可以概括为一个接口、四种实现和六种方法。

1个接口

上下文指定了上下文的四种基本方法。

四种实现

EmptyCtx实现了一个空上下文,可以用作根节点。

CancelCtx实现了一个带有取消功能的上下文,可以主动取消。

TimerCtx实现了一个由定时器timer和deadline取消的上下文。

valueCtx实现了一个上下文,在该上下文中可以通过键和值字段存储数据。

六种方法

后台返回一个emptyCtx作为根节点。

TODO将emptyCtx作为未知节点返回

WithCancel返回一个cancelCtx。

WithDeadline返回一个timerCtx。

WithTimeout返回一个timerCtx。

With返回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实现了一个空的Context接口,它的主要作用是为background和todo方法返回预先初始化的私有变量Background和TODO,这些变量将在同一个Go程序中重用:

var(Background=new(emptyCtx)TODO=new(emptyCtx))func Background()Context { return Background } func TODO()Context { return TODO }

后台和TODO在实现上没有区别,只是使用的语义不同:

背景是上下文的根节点;

O TODO只应在您不确定应使用哪个上下文时使用;

3.3取消tx

CancelCtx实现了取消器接口和上下文接口:

类型取消器接口{ cancel(removefrompertanchor bool,err error) Done() -chan struct{}}

其结构如下:

类型cancelCtx struct {//直接嵌入一个上下文,那么cancelCtx可以看作是一个上下文上下文musync.mutex//protections以下字段完成atomic.value//of chan struct { },惰性创建,通过第一次取消调用关闭子映射[canceler]struct{} //通过第一次取消调用设置为nil err error//通过第一次取消调用设置为非nil }

我们可以使用WithCancel方法来创建cancelCtx:

func with cancel(parent Context)(CTX Context,cancel cancel func){ if parent==nil { panic(无法从nil父级创建上下文)} c:=newCancelCtx(parent)propagate cancel(parent,c) return c,func() { c.cancel(true,Canceled)} } func newCancelCtx(parent Context)cancel CTX { return cancel CTX { Context:parent } }

上面的方法,我们传入一个父context(这通常是一个后台,作为根节点),返回新创建的Context,以闭包的形式返回一个cancel方法。

NewCancelCtx将传入的上下文包装到一个私有结构context.cancelCtx中

PropagateCancel构建父上下文和子上下文之间的关系,以形成一个树形结构。当父上下文被取消时,子上下文也将被取消:

c传播取消(父上下文,子取消器){//1。如果父ctx是不可撤销的ctx,那么直接返回,不进行关联done:=parent . done()if done==nil { return//parent永不取消}//2。然后,判断父ctx是否已经取消。选择{case -done: //2.1如果上级ctx已经取消,则不需要关联。//那么这里也应该顺便取消子ctx,因为父ctx应该取消子ctx。//这里需要手动触发取消,因为还没有关联。//父级已经是取消的子级。Cancel (false,Parent.err())返回默认值:}//3。从父Ctx中提取cancelCtx,并将子ctx添加到父ctx的子ctx if p,ok:=parentCancelCtx(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。开始一个新的取消信号atomic.addint32 (goroutines,1)go func(){ select { case-parent . done():child . cancel(false,parent . 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表示这个ctx对于nil是可取消的//如果done==closedchan表示这个ctx不是标准的cancelCtx,也许它一个自定义if done==closed chan | | done==nil { return nil,false }//然后调用value方法从ctx中提取cancelctxp,ok:=parent . value(cancel txkey)。(* cancelctx)如果!Ok {return nil,false} //最后判断cancelCtx中存储的done与父Ctx中的done是否一致。//如果不是,则父级不是CancelCTX P done,_:=p.done.load()。(chan struct {}) 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 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中存储的值一致,则直接返回;如果没有,将在父代中找到。最后找到根节点(通常是emptyCtx)直接返回一个nil。所以在使用Value方法时,需要判断结果是否为nil,类似于链表,效率很低。不建议传递参数。

使用建议

在官方博客中,提出了一些使用语境的建议:

唐不要把上下文塞进结构中。上下文类型直接作为函数的第一个参数,一般命名为ctx。

唐不要向函数传递一个nil上下文。如果你真的不不知道传递什么,标准库已经为你准备了一个上下文:TODO。

唐不要将应该用作函数参数的类型插入到上下文中。上下文应该存储一些公共数据。例如:登录会话、cookie等。

相同的上下文可以传递给多个goroutine。唐别担心,上下文是并发安全的。

审计彭静