Go-知识并发控制Context

Go-知识并发控制Context

  • 1. 介绍
  • 2. 实现原理
    • 2.1 接口定义
    • 2.2 Deadline()
    • 2.3 Done()
    • 2.4 Err()
    • 2.5 Value()
  • 3. 空 context
  • 4. cancelCtx
    • 4.1 Done()
    • 4.2 Err()
    • 4.3 cancel()
    • 4.4 WithCancel
    • 4.5 例子
    • 4.6 总结
  • 5. timerCtx
    • 5.1 Deadline
    • 5.2 cancel
    • 5.3 WithDeadline
    • 5.4 WithTimeout
    • 5.5 例子
    • 5.6 总结
  • 6. valueCtx
    • 6.1 Value
    • 6.2 WithValue
    • 6.3 例子
    • 6.4 总结
  • 7. afterFuncCtx
    • 7.1 cancel
    • 7.2 AfterFunc
    • 7.3 总结
  • 8. withoutCancelCtx
    • 8.1 WithoutCancel
  • 9. 总结

gitio: https://a18792721831.github.io/

1. 介绍

Go 语言的 context 是应用开发常用的并发控制技术,它与 WaitGroup 最大的不同点是 context
对于派生 goroutine 有更强的控制力,可以控制多级的 goroutine 。
context 翻译成中文是 上下文 ,即可以控制一组呈树状结构的 goroutine ,每个 goroutine 拥有相同的上下文。
在这里插入图片描述

上图中由于 goroutine 派生出子 goroutine ,而子 goroutine 又继续派生出新的 goroutine ,
这种情况下使用 WaitGroup 就不太容易,因为子 goroutine 的个数不太容易确定,而使用 context 就很容易实现。

2. 实现原理

context 实际上只定义了接口,凡是实现该接口的类都可以称为一种 context ,官方包中实现了几个常用的 context ,分别用于不同的场景。

2.1 接口定义

源码包中的 src/context/context.go定义了接口:
在这里插入图片描述

基础的 context 接口只定义了4个方法。

2.2 Deadline()

Deadline() (deadline time.Time, ok bool)
该方法返回一个 deadline 和标识是否已设置 deadline 的 bool 值,如果没有设置 deadline ,
则 ok == false ,此时 deadline 是一个初始值的 time.Time 值。

2.3 Done()

Done() <-chan struct{}
该方法返回一个用于探测 Context 是否取消的 channel , 当 Context 取消时会自动将该 channel 关闭。
对于不支持取消的 Context ,该方法可能会返回 nil。

2.4 Err()

Err() error
该方法描述 context 关闭的原因。关闭原因由 context 实现控制,不需要用户设置。比如 Deadline context,
关闭原因可能是因为 deadline ,也可能是提前被主动关闭,那么关闭原因就会不同:

  • 因 deadline 关闭:context deadline exceeded.
  • 因主动关闭: context canceled.

当 context 关闭后,Err()返回 context 的关闭原因;当 context 还未关闭时,Err() 返回nil.

2.5 Value()

Value(key any) any
有一种 context ,它不是用于控制呈树状分部的 goroutine ,而是用于在树状分部的 goroutine 间传递信息。
Value() 方法就是用于此种类型的 context ,该方法根据 key 值查询 map 中的 value 。

3. 空 context

context 包中定义了一个空的 context ,名为 emptyCtx ,用于 context 的根节点,空 context 只是简单地实现了 Context,
本身也不包含任何值,仅用于其他 context 的父节点
在这里插入图片描述

context 包中还定义了一个公用的 emptyCtx 全局变量,名为 background ,可以使用
context.Background()获取。

在这里插入图片描述

context 包提供了四个方法创建不同类型的 context ,使用这四个方法时,如果没有父 context,
则都需要传入 background ,即将 background 作为其父节点:

  • WithCancel()
  • WithDeadline()
  • WithTimeout()
  • WithValue()

context 包中实现了 Context 接口的 struct,除了 emptyCtx ,还有 cancelCtx,timerCtx和valueCtx三种,
基于这三种 context 实例,实现了上述四种额理性的 context.
context 包中各 context 类型之间的关系:

在这里插入图片描述

4. cancelCtx

源码包中的 src/context/context.go:cancelCtx定义了该类型 context:
在这里插入图片描述

children 中记录了由此context派生的所有child , 此 context 被 cancel 时,会把其中所有的 child 都 cancel 。
cancelCtx 与 deadline 和 value 无关,所以只需要实现 Done() 和 Err() 接口即可。

4.1 Done()

按照 Context 的定义,Done()方法只需要返回一个 channel 即可,对于 cancelCtx 来说
只需要返回成员变量 done 即可。

func (c *cancelCtx) Done() <-chan struct{} {// 获取 成员变量 done , 在老版本中,done 直接是 类型 chan struct{} 类型,不安全,新版本是 atomic.Valued := c.done.Load()// 如果 不为 nil 表示已经被初始化过了,直接返回if d != nil {return d.(chan struct{})}// 如果还未初始化,那么加锁c.mu.Lock()defer c.mu.Unlock()// 加锁重新获取数据,防止并发导致的泄露,加了锁之后再重新获取数据,一定是最新的,加锁之前获取的数据,不一定是最新的d = c.done.Load()if d == nil {// 初始化 done 变量 的 channel d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}

在老版本中 Done 每次都必须加锁,然后获取值,解锁,返回。
在这里插入图片描述

在 Done 方法中,只有第一次需要初始化,后面都是直接返回即可,所以只有第一次的加锁是有效的,后面加锁都是无效的(假设 channel 不会被修改)
因此在新版本中,done 变量使用 atomic.Value 存储,并发安全,在Done 里面也是乐观获取,如果获取得到为空,那么在加锁初始化。

既然只有第一次才初始化,为何不在 init 方法中初始化呢?

在 cancelCtx 中 ,done 变量就是 channel ,channel 会经历 nil -> make -> close 三个阶段。
如果直接 close 值为 nil 的 channel ,会引发 panic 。

4.2 Err()

按照 Context 定义,Err() 需要返回一个 error 告知 context 被关闭的原因。 对于 cancelCtx 来说
返回成员变量 err 即可。

func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}

Err()也是加锁然后获取值,解锁。

4.3 cancel()

在 Context 接口定义中并没有 cancel 方法,所以 cancel方法是在接口canceler中定义的。
在这里插入图片描述

cancelCtx 和 timerCtx 实现了这个接口。
cancelCtx 接口和 Context 接口都定义了 Done() 方法,而且完全相同。
cancel 方法是 cancelCtx 的关键方法,作用是关闭自己和其后代,其后代存储在 cancelCtx.children 的map中,
其实可以理解是 Set 中,因为map的value没有任何意义,数据本身是存储在key中的。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {// 必须给出 cancel 的原因if err == nil {panic("context: internal error: missing cancel error")}// 加锁c.mu.Lock()// 读取 err if c.err != nil {c.mu.Unlock()// 已经 cancel 了return // already canceled}// 否则将 error 设置到 成员变量中c.err = err// 读取 done 成员变量d, _ := c.done.Load().(chan struct{})// 如果 done 成员变量为空,表示还未调用 Done() 就调用 cancel,那么需要先初始化 done 成员变量// 因为 close channel 时,channel 不能nil 否则触发 panicif d == nil {// 这里的 closedchan 是一个全局变量,并且在 init 中就 close 了c.done.Store(closedchan)} else {// 如果是已经初始化的 channel ,那么调用 close close(d)}// 将 cannel 扩散到全部的后代中// 需要注意的是,这里是递归扩散,如果 children 也有 children ,那么孙子也会执行 cannelfor child := range c.children {// 注意: 获取孩子的锁,同时持有父母的锁。// 移出 后代,不需要移出自己本身child.cancel(false, err)}// 同时将 后代移出 mapc.children = nil// 解锁c.mu.Unlock()// 如果需要将自己从 parent 中删除if removeFromParent {removeChild(c.Context, c)}
}

这是 closedchan的源码,在初始化的时候就 close 了。
在这里插入图片描述

对于 removeChild的实现,这里先留一下,要不不太好理解。

4.4 WithCancel

WithCancel 方法做了三件事:

  • 初始化一个 cancelCtx 实例
  • 将 cancelCTx 实例添加到其父节点的 children 中(如果父节点也可以被cancel)
  • 返回 cancelCtx 实例和 cancel() 方法
// WithCancel返回带有新的Done通道的父副本。返回的
// 当调用返回的cancel函数时,上下文的Done通道关闭
// 或当父上下文的Done通道关闭时,以先发生者为准。
//
// 取消此上下文会释放与其关联的资源,因此代码应该
// 在此上下文中运行的操作完成后,立即调用cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)return c, func() { c.cancel(true, Canceled, nil) }
}

这里引入了 CancelFunc 类型

// CancelFunc告诉操作放弃它的工作。
// CancelFunc不等待工作停止。
// 一个CancelFunc可以被多个goroutines同时调用.
// 第一次调用后,对CancelFunc的后续调用不执行任何操作。
type CancelFunc func()

前面看到 cancel 方法是包内可见,但是 cancel 方法又需要包外的调用者触发,而且在触发的时候,入参又有一定的要求。
基于这种需要,这里使用 func 类型包装,在获取 func 的时候,将入参做个处理,最后将 入参 经过处理的 cancel 用 func 变量的方式
返回到调用者,至于触发 cancel 的时机,由调用者决定。 换句话来说,调用者只能决定 cancel 的时机和部分受限的参数。
这种使用方式很巧妙,值得好好学学。
首先看 withCancel(parent)调用

func withCancel(parent Context) *cancelCtx {// 父节点不能是 nilif parent == nil {panic("cannot create context from nil parent")}// 创建当前节点c := &cancelCtx{}// 将当前节点加入到树中,也就是加入到 父节点 的 children map中c.propagateCancel(parent, c)return c
}

OK,接着看下 propagateCancel方法

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {// 将 父节点设置为 parentc.Context = parent// 获取父节点的 channel ,因为整颗树使用一个 channel done := parent.Done()// 如果父节点的 channel 是空的,说明 父节点永远不会 cancel,也就是根节点(emptyCtx?)// 那也就是说 当前 节点是 一代节点,因为父节点 可能是 emptyCtxif done == nil {return // parent is never canceled}// 使用 select 阻塞获取 channel 的值,但是因为有 default// 所以这里可以理解为 非阻塞获取 channel 值,如果没有获取到,什么也不做select {// 如果 channel 有值,表示 父节点执行了 cancel,那么子节点也需要执行 cancelcase <-done:// parent is already canceled// 这里需要注意,扩散的时候,都是不把自己从父节点列表中删除,也就是不把自己从树里面移出child.cancel(false, parent.Err(), Cause(parent))returndefault:}// 如果父节点没有 cancel ,那就就把当前节点加入父节点的 children// 如果父节点已经 cancel ,那么当前节点也 cancel ,并且 不加入父节点的 children// parentCancelCtx 是查找祖辈节点是否是 cancelCtx (包括包装的cancelCtx)// 祖辈节点的查找通过 Value实现的if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()// 判断 祖辈节点是否 cancelif p.err != nil {// parent has already been canceled// 祖辈节点存在 cancel,那么 当前节点也 cancel// 当前节点理论上还未加入到 children中,执行 cancel 是为了保证 cancel 节点的数据完整性child.cancel(false, p.err, p.cause)} else {// 如果祖辈节点没有 cancel ,而且祖辈节点还没有 children// 那么初始化if p.children == nil {p.children = make(map[canceler]struct{})}// 将当前节点加入到 children中// 如果不加入,那么当 祖辈节点 cancel 的时候,就无法通知到当前 节点p.children[child] = struct{}{}}p.mu.Unlock()return}// 如果 父节点 不是 cancelCtx ,而是实现了 afterFuncer 接口的 stopCtx// stopCtx 需要实现 AfterFunc(func()) func() bool 方法,获取 cancel 后需要执行的 funcif a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()// 如果 父节点是 stopCtx,那么当前节点也 包装成 stopCtx// 但是需要注意的是,当前节点本质上还是 cancelCtxstop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})// 将当前节点包装为 stopCtxc.Context = stopCtx{Context: parent,stop:    stop,}c.mu.Unlock()return}// 表示整个树中有多少个节点,简单记录,包内可见,用于 test goroutines.Add(1)// 如果从当前节点的父节点不是 cancelCtx,那么启动一个协程// 协程的作用是等待 父节点 结束,然后结束当前节点。// context 一个很重要的作用就是可扩散// 如果 父节点不是 cancelCtx(stopCtx 可以认为是包装的 cancelCtx)// 但是有 channel ,那么就需要扩散传播// 那么就需要格外的协程去 canncel // 因为当前节点是 cancelCtx,所以在当前这个路径下继续增加节点,就不会在创建协程了。go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}

在上面的实现中,parentCancelCtx也是一个关键方法,看看如何找到祖辈的 cancelCtx

func parentCancelCtx(parent Context) (*cancelCtx, bool) {// 获取父节点的 channeldone := parent.Done()// done 为空表示父节点不传播扩散或是到了根节点// done 为 closedchan 表示父节点是 cancelCtx,但是很可惜,父节点 cancelCtx 已经 cancel 了if done == closedchan || done == nil {// 父节点已经调用了 cancel ,那么当前节点就等待 父节点 cancel ,然后当前节点 cancel// 逐层 扩散?return nil, false}// 找祖辈节点// 因为任何 Context 都需要实现 Value 方法// cancelCtx 的 Value 方法就是将当前节点放入 指定的 key // 因为在 cancelCtx 中,每个 节点的 key 都是 cancelCtxKey,所以这里就像是循环查找,直到根节点p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)// 如果 父节点不是 cancelCtx ,那么返回未找到if !ok {return nil, false}// 获取 cancelCtx 的 done 变量pdone, _ := p.done.Load().(chan struct{})// 二次确认,Done() 返回的就是 done 变量if pdone != done {return nil, false}// 返回找到了return p, true
}

接着看一下 Value方法:

func (c *cancelCtx) Value(key interface{}) interface{} {// 假设 r -> a -> b -> c// a,b,c 三个节点的 key 都是 同样的值// 所以这里就相当于是从当前节点一直向上查找,直到根节点if key == &cancelCtxKey {return c}// 父节点的 Value 方法return c.Context.Value(key)
}

简单说一下 WithCancel 的逻辑:

  • 初始化一个cancelCtx实例
  • 将cancelCtx 实例添加到父节点的 children中(如果父节点也可以被 cancel )
  • 返回cancelCtx实例和 cancel方法

在添加和查找父节点时,逻辑如下:

  • 如果父节点支持 cancel ,那么父节点有 children 变量,那么将当前节点也加入到 children中
  • 如果父节点不支持 cancel ,那么就说明父节点没有 children 变量,就不能将当前节点加入到 children 中
  • 如果该路径中没有 children 变量,那么就新增一个协程,等待父节点(cancelCtx)结束

之前存疑的removeChild就是将触发节点从整个路径中移除。那么被移除节点的子节点自然就从整个树上面移除了。
其实严格来说,不一定是树形结构,也有可能是图形结构。

4.5 例子

单链传播

func TestCtxEmpty(t *testing.T) {ctx := context.Background()ctxr, cancelr := context.WithCancel(ctx)// root -> n1 -> n2 -> c3 -> c4 -> n5 -> c6go func() {ctx1, _ := context.WithCancel(ctxr)fmt.Println("ctx1 <- ctxr")go func() {ctx2, _ := context.WithCancel(ctx1)fmt.Println("ctx2 <- ctx1")go func() {fmt.Println("ctx3 <- ctx2")select {case <-ctx2.Done():fmt.Println("ctx3 <- ctx2 done")return}}()select {case <-ctx1.Done():fmt.Println("ctx2 <- ctx1 done")return}}()select {case <-ctxr.Done():fmt.Println("ctx1 <- ctxr done")return}}()time.Sleep(time.Second)cancelr()time.Sleep(time.Second)
}

执行如下:
在这里插入图片描述

树形传播

func TestCtxEmpty(t *testing.T) {ctx := context.Background()// r -> 1a -> 2a//         -> 2b//   -> 1bctxr, cancelr := context.WithCancel(ctx)go func() {fmt.Println("ctx1a <- ctxr")ctx1, _ := context.WithCancel(ctxr)go func() {fmt.Println("ctx2a <- ctx1")select {case <-ctx1.Done():fmt.Println("ctx2a <- ctx1 done")return}}()go func() {fmt.Println("ctx2b <- ctx1")select {case <-ctx1.Done():fmt.Println("ctx2b <- ctx1 done")return}}()select {case <-ctxr.Done():fmt.Println("ctx1a <- ctxr done")return}}()go func() {fmt.Println("ctx1b <- ctxr")select {case <-ctxr.Done():fmt.Println("ctx1b <- ctxr done")return}}()time.Sleep(time.Second)cancelr()time.Sleep(time.Second)
}

执行结果
在这里插入图片描述

4.6 总结

cancelCtx 实现了 Context 和 canceler 接口。
Context 接口定义了 Done, Value, Deadline, Err 方法
canceler 接口定义了 Done, cancel 方法

emptyCtx 是一个空值实现 Context 的 结构体,主要用作根节点。

cancelCtx 的结构体组合了 Context 接口,相当于是隐含成员变量,作为父节点。
cancelCtx 的成员变量 done 存储了 Context 接口中 Done 方法返回的 channel 作为传播变量。
cancelCtx 通过 WithCancel 方法创建,通过传入 Context 类型的 parent 作为父节点。
cancelCtx 的成员变量 children 存储了子 canceler 节点,当发生 cancel 的时候,会进行传播调用。
cancelCtx 通过 WithCancel 进行组织,可以是链表,可以是树,可以是图,但是关系越复杂,越容易出现环形死锁。
cancelCtx 通过 WithCancel 方法创建的时候,返回的第二个值是触发cancel的func,调用会触发该路径下全部子节点的 cancel。
cancelCtx 的cancel方法通过func值传递给调用者,由调用者确定时机,这是一种很巧妙的将内部方法交由外部调用的方式,值得学习。
cancelCtx 实现了Value 方法,通过将全部的 cancelCtx 的 key 设置成相同的,以此实现从当前节点查找整个路径。

5. timerCtx

源码包中的src/context/context.go:timerCtx定义了timerCtx:

// timerCtx携带一个计时器和一个截止时间。它将cancelCtx嵌入到
// 实现Done和Err。它通过停止其计时器来实现取消,然后
// 委托t o cancelCtx.ca ncel。
type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

timerCtx 在cancelCtx 的基础上增加了 deadline ,用于标识自动 cancel 的最终时间,
而timer就是一个触发自动cancel 的定时器。
并由此衍生出来WithDeadline() 和 WithTimeout()。
实现上这两种类型原理相同,但是在使用的时候存在区别。

  • deadline: 指定最后期限,比如 context 将在指定时间结束
  • Timeout: 指定最大存活时间,比如 context 将在 2s 后结束

5.1 Deadline

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}

直接返回 timerCtx 结构体中的 deadline 。

5.2 cancel

func (c *timerCtx) cancel(removeFromParent bool, err error) {// 调用 cancelCtx 的 cancel 方法,并且当前节点不从父节点删除c.cancelCtx.cancel(false, err)if removeFromParent {removeChild(c.cancelCtx.Context, c)}c.mu.Lock()// 如果存在计时器,那么停止计时器// 这是还未到时间,就触发 cancel 了,调用者触发的if c.timer != nil {c.timer.Stop()c.timer = nil}c.mu.Unlock()
}

对于 timerCtx ,不一定只有 timer 触发 cancel , 因为 timerCtx 组合了 cancelCtx ,所以还有可能是 调用者手动 cancel.

5.3 WithDeadline

// WithDeadline返回父上下文的副本,并调整了截止日期
// 不晚于d。如果父母的截止日期已经早于d,
// WithDeadline(parent,d) 在语义上等同于parent。返回的
// 上下文的完成通道在截止日期到期时关闭,当返回的
// 调用cancel函数,或者当父上下文的Done通道为
// 关闭,以先发生者为准。
//
// 取消此上下文会释放与其关联的资源,因此代码应该
// 在此上下文中运行的操作完成后,立即调用cancel。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {// 如果父节点为空,那么异常,父节点不能为空if parent == nil {panic("cannot create context from nil parent")}// 判断父节点的时间和当前节点的时间if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.// 父节点的时间早于当前节点// 那么只需要保证父节点 cancel ,当前节点也 cancel // 当前节点的定时器没有意义,直接认为当前节点是 cancelCtxreturn WithCancel(parent)}// 当前节点的时间比父节点的时间晚,需要创建 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}// 将当前节点加入父节点propagateCancel(parent, c)// 计算时间差dur := time.Until(d)// 如果时间差小于等于0,表示时间到了(可能加入节点或创建当前节点比较耗时)if dur <= 0 {// 当前节点从父节点移除,同时设置 error 是 到时间 了c.cancel(true, DeadlineExceeded) // deadline has already passed// 否则返回 timerCtx 和 cancelFuncreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()// 加锁读取 err , err 为空表示还未取消或到时间if c.err == nil {// 增加定时器执行 funcc.timer = time.AfterFunc(dur, func() {// 触发 cancel c.cancel(true, DeadlineExceeded)})}// 返回 timerCtx 和 cancelFuncreturn c, func() { c.cancel(true, Canceled) }
}

5.4 WithTimeout

WithTimeout实际上是复用了WithDeadline: deadline = now + timeout
在这里插入图片描述

5.5 例子

timeout

func TestTimerCtx(t *testing.T) {ctxr := context.Background()ctx1, cancel1 := context.WithTimeout(ctxr, time.Second)go func() {fmt.Println("timerCtx ctx1 1 second timeout")select {case <-ctx1.Done():fmt.Println("timerCtx ctx1 1 second done")return}}()defer cancel1()ctx2, cancel2 := context.WithTimeout(ctxr, time.Second*3)defer cancel2()select {case <-ctx2.Done():fmt.Println("main timeout 3")return}
}

执行如下:
在这里插入图片描述

5.6 总结

timerCtx 组合了 cancelCtx ,并在其基础上增加了 time 定时器,用于触发 cancel ,从而实现了 deadline 和 timeout 两种能力。

timerCtx 实现了 Context 的 Deadline 方法,直接返回了timerCtx 的成员变量 deadline.
timerCtx 组合了 cancelCtx ,如果父节点的时间早于子节点,子节点会退化成 cancelCtx;如果子节点的deadline 大于父节点,那么子节点会增加 time 定时器,用于触发 cancel

timerCtx 有两种方式触发,一种时间计时器触发,一种是调用者触发;计时器触发,原因为 context deadline exceeded,手动取消触发为 context canceled

timerCtx 的 timeout 复用了 deadline 实现方式: deadline = now + timeout

6. valueCtx

源码包src/context/context.go:valueCtx定义了valueCtx:
在这里插入图片描述

valueCtx 只是在 Context 的基础上增加了一个 key-value 对,用于在各级协程间传递一些数据。
由于 valueCtx 即不需要 cancel ,也不需要 deadline ,那么只需要实现 Value 方法即可。

6.1 Value

valueCtx 的结构非常简单,只是在 Context 的基础上增加了 key 和 value 两个成员变量,所以 Value 方法也很简单:

func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

如果当前节点的 key 等于请求的 key,那么返回当前节点的 value 。
如果当前节点的 key 不等于请求的 key, 那么会递归向上查找 ,直到找到根节点。 找不到就返回 interface{}.

6.2 WithValue

WithValue也是非常的简单,创建节点,然后设置 key, value 然后将节点挂载树上。

// WithValue返回parent的副本,其中与key关联的值为
// 瓦尔。
//
// 仅对请求范围内的数据使用上下文值,这些数据传输进程和
// Api,不用于将可选参数传递给函数。
//
// 提供的键必须具有可比性,并且不应为类型
// 字符串或任何其他内置类型,以避免之间的冲突
// 使用上下文的包。WithValue的用户应该定义自己的
// 键的类型。以避免在分配给
// interface {}, 上下文键通常具有具体类型
// struct{}。或者,导出的上下文键变量的静态
// 类型应该是指针或接口。
func WithValue(parent Context, key, val interface{}) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}// 需要注意的一点,key 必须是可比较的if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}// 将新增的节点加入,并设置 key, value return &valueCtx{parent, key, val}
}

6.3 例子

func TestValueCtx(t *testing.T) {ctx := context.Background()ctxV1 := context.WithValue(ctx, "hi", "hi")ctxV2 := context.WithValue(ctxV1, "hello", "hello")go func() {fmt.Println(ctxV2.Value("hello").(string))fmt.Println(ctxV2.Value("hi").(string))}()time.Sleep(time.Second)
}

执行结果
在这里插入图片描述

其实更好的用法应该是这样

func TestValueCtx(t *testing.T) {ctx := context.Background()ctx = context.WithValue(ctx, "hi", "hi")ctx = context.WithValue(ctx, "hello", "hello")go func() {fmt.Println(ctx.Value("hello").(string))fmt.Println(ctx.Value("hi").(string))}()time.Sleep(time.Second)
}

从始至终都使用一个变量 ctx ,像是 ctx 是一个 map 一样。

6.4 总结

valueCtx 主要是在 Context 的基础上实现了 Value 方法,其结构体增加了 key 和 value 两个成员变量。

valueCtx 只能增加不能减少键值对。
valueCtx 用树形结构不断做加法来存储多个键值对。
valueCtx 的树形结构中,一个节点只能存储一个键值对。
valueCtx 的key必须是可比较的。
valueCtx 查找的时候,从当前节点向根节点遍历查询,如果比较深的话,可能会存在性能问题。
valueCtx 的实现中无锁。

7. afterFuncCtx

定义:

type afterFuncCtx struct {cancelCtxonce sync.Once // either starts running f or stops f from runningf    func()
}

afterFuncCtx 在 cancelCtx 的基础上增加了附加的收尾操作,以及执行标记。
afterFuncCtx 在触发 cancel 后执行一次附加操作,用于类似清理回收资源等操作。

7.1 cancel

func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {// 执行 cancelCtx 的 cancel 操作a.cancelCtx.cancel(false, err, cause)if removeFromParent {removeChild(a.Context, a)}// 执行一次 给定的 func a.once.Do(func() {go a.f()})
}

afterFuncCtx 重写了 cancelCtx 的 cancel 方法,在基础上进行了增强。

7.2 AfterFunc

AfterFunc用于创建 afterFuncCtx .

// AfterFunc安排在ctx完成后在自己的goroutine中调用f
// (取消或超时)。
// 如果ctx已经完成,AfterFunc会立即在自己的goroutine中调用f。
//
// 在一个上下文上多次调用AfterFunc独立操作;
// 一个不取代另一个。
//
// 调用返回的stop函数停止ctx与f的关联。
// 如果调用停止运行f,则返回true。
// 如果stop返回false,
// 要么上下文已完成,并且f已在其自己的goroutine中启动;
// 或f已经停止。
// 停止函数不等待f完成后返回。
// 如果调用者需要知道f是否完成,
// 它必须明确地与f协调。
//
// 如果ctx有一个 “AfterFunc(func()) func() bool” 方法,
// AfterFunc将使用它来安排调用。
func AfterFunc(ctx Context, f func()) (stop func() bool) {// 创建 afterFuncCtxa := &afterFuncCtx{f: f,}// 加入父节点中a.cancelCtx.propagateCancel(ctx, a)// 构造 终止函数return func() bool {// 是否停止的标志stopped := false// 使用 once.Do 执行,如果 f 函数已经启动,那么 once.Do 就不会执行, stopped 是 false // 如果 f 函数还未执行,那么 once.Do 执行,会将 stopped 设置为 truea.once.Do(func() {stopped = true})// 如果 stopped 为 true ,表示 f 函数还未执行,而且因为 once.Do 已经执行了 上面的 stoppped = true ,所以也就不会执行 f 了if stopped {// f 还未执行,直接触发 cancel a.cancel(true, Canceled, nil)}// 返回停止标志return stopped}
}

7.3 总结

afterFuncCtx 在 cancelCtx 的基础上增加了 sync.Once 和 调用者指定 func 的成员变量,在 cancel 触发之后执行一次 指定的 func .

afterFuncCtx 重写了 cancelCtx 的 cancel 函数,增强了cancel 的能力,以便执行附加操作。
afterFuncCtx 利用 sync.Once 的只执行一次的特性,用于调度 stop 或 附加func
afterFuncCtx 如果已经开始执行 附加func ,那么stop操作无法终止 附加func

afterFuncCtx 非常像给 cancelCtx 打了个补丁,增强了能力。

8. withoutCancelCtx

上面那几种实现都是基于 cancelCtx 的实现,除此之外,还有不依赖 cancelCtx 的实现

type withoutCancelCtx struct {c Context
}

其 Context 的实现和 emptyCtx 的实现几乎相同。

8.1 WithoutCancel

// WithoutCancel返回父级的副本,当父级被取消时,它不会被取消。
// 返回的上下文不返回Deadline或Err,其Done channel为nil。
// 在返回的上下文上调用 [原因] 返回nil。
func WithoutCancel(parent Context) Context {if parent == nil {panic("cannot create context from nil parent")}return withoutCancelCtx{parent}
}

使用 WithoutCancel 进行创建节点,加入树形结构,再无其他能力。

9. 总结

说了这么多的 Ctx ,其实只是 SDK 包实现的几种通用的实现,而且加上每种实现都是 Context 的子节点,
这就能让上述全部的 Ctx 进行相互自由组合,以便应用到不同的场景需要中。
学习 Context 不仅仅学习 Context ,更学习 SDK 的一些实现方式。
通过 返回 func ,实现让调用者调用内部的方法,甚至可以让调用者控制调用时机,但是参数相对受限。
学习如何更好的利用接口和 struct 实现多变组合。
学习组合增强的方式,在不改变原逻辑的前提下,增强能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/20185.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML+CSS+JS 熊猫登录表单

效果演示 实现了一个可爱的熊猫登录界面,页面背景使用了渐变色,熊猫的头部和身体使用了圆形和椭圆形的边框,使用了CSS的伪元素和阴影效果来实现熊猫的手和脚。登录框使用了flex布局,包括用户名和密码的输入框和登录按钮,使用了CSS的过渡效果和伪类来实现输入框的动态效果。…

【LeetCode刷题】二分查找:山脉数组的峰顶索引、寻找峰值

【LeetCode刷题】Day 13 题目1&#xff1a;852.山脉数组的峰顶索引思路分析&#xff1a;思路1&#xff1a;暴力枚举O(N)思路2&#xff1a;二分查找O(logN) 题目2&#xff1a;162.寻找峰值思路分析&#xff1a;思路1&#xff1a;二分查找O(logN) 题目1&#xff1a;852.山脉数组的…

(二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2

层序遍历 10 102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 综合代码&#xff1a; class Solution{public List<List<Integer>> resList new ArrayList<List<Integer>>();public List<List<…

24年海南三支一扶报名流程步骤详解

一、考试时间安排&#xff1a; 报名时间&#xff1a;6月1日8:00至6月7日18:00 准考证打印时间&#xff1a;6月17日8:00 考试时间&#xff1a;6月22日 二、招聘人数 海南省计划招募390名高校毕业生 三、笔试内容&#xff1a; 笔试内容&#xff1a;综合能力和素质&#xff08;满分…

【设计模式】JAVA Design Patterns——Iterator(迭代器模式)

&#x1f50d;目的 提供一种在不暴露其基础表示的情况下顺序访问聚合对象的元素的方法。 &#x1f50d;解释 真实世界例子 百宝箱包含一组魔法物品。有多种物品&#xff0c;例如戒指&#xff0c;药水和武器。可以使用藏宝箱提供的迭代器按类型浏览商品。 通俗描述 容器可以提供…

【AIGC-数字人】V-Express:渐进式训练的数字人视频生成技术

介绍 在人像视频生成领域&#xff0c;使用单张图像生成人像视频已经变得越来越普遍。一种常见的方法涉及利用生成模型来增强适配器以实现受控生成。然而&#xff0c;控制信号的强度可能会有所不同&#xff0c;包括文本、音频、图像参考、姿态、深度图等。其中&#xff0c;较弱的…

Vue.js - 生命周期与工程化开发【0基础向 Vue 基础学习】

文章目录 Vue 的生命周期Vue 生命周期的四个阶段Vue 生命周期函数&#xff08;钩子函数 工程化开发 & 脚手架 Vue CLI**开发 Vue 的两种方式&#xff1a;**脚手架目录文件介绍项目运行流程组件化开发 & 根组件App.vue 文件&#xff08;单文件组件&#xff09;的三个组成…

LeetCode 图-岛屿问题

图 图的基本知识基本概念图的类型相关术语 图的存储 LeetCode 相关题目岛屿问题岛屿的最大面积岛屿的周长 图的基本知识 基本概念 图的类型 无向图有向图加权图 相关术语 顶点边路径路径长度环负权环连通性顶点的度入度出度 图的存储 邻接矩阵存储&#xff1a;是用一个二…

真机调试 Error:系统错误,xxx exceed max limit 2MB

我们在使用微信开发者工具开发小程序、小游戏等应用时&#xff0c;往往会点击“真机调试”&#xff0c;微信扫描查看真实情况。 但是会出现下面的报错提示&#xff0c;是因为主包体积超过了2MB。 小程序有体积和资源加载限制&#xff0c;在微信小程序中&#xff0c;每个包不能…

vue3简单快速实现主题切换功能

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《vue3实战》 目录 内容概要 实现步骤 1.定义不同主题的css样式变量 2.入口main.ts中引入这个样式文件 3.主题样式css变量引用 4.设置默认主题样式 5.实现点击按钮主题切换 总结 最近发现了一个巨牛的人工智…

【Linux-buildroot,】

Linux-buildroot, ■ buildroot■ 1、简介■ 2、下载■ 2、编译■ 问题一&#xff1a;buildroot 编译的时候会先从网上下载所需的软件源码&#xff0c;下载cmake-3.8.2.tar.gz或下载很慢的情况 ■ buildroot-构建根文件系统■ 1、配置 buildroot■ 2、■ 3、 ■ buildroot-构建…

TK防关联引流系统:全球TikTok多账号运营的神器

在TikTok的生态中&#xff0c;高效运营多个账号已成为品牌全球推广的必经之路。为此&#xff0c;TK防关联引流系统应运而生&#xff0c;它是一款专为TikTok设计的效率神器&#xff0c;助您迅速搭建并管理全球多账号矩阵。该系统由先进的“防关联智能终端”硬件和智能的“TK防关…

AI生成微信职业头像

加油&#xff0c;新时代打工人&#xff01; 真别说&#xff0c;还挺好看的 https://chatglm.cn/main/alltoolsdetail

GPT-4o:免费且更快的模型

OpenAI GPT-4o 公告 OpenAI 推出了增强版 GPT-4 模型——OpenAI GPT-4o&#xff0c;用于支持 ChatGPT。首席技术官 Mira Murati 表示&#xff0c;更新后的模型速度更快&#xff0c;并在文本、视觉和音频处理方面有了显著提升。GPT-4o 将免费向所有用户开放&#xff0c;付费用户…

乐高小人分类项目

数据来源 LEGO Minifigures | Kaggle 建立文件目录 BASE_DIR lego/star-wars-images/ names [YODA, LUKE SKYWALKER, R2-D2, MACE WINDU, GENERAL GRIEVOUS ] tf.random.set_seed(1)# Read information about dataset if not os.path.isdir(BASE_DIR train/):for name in …

52 https

HTTPS是什么 https也是一个应用层协议&#xff0c;是在http协议的基础上引入了一个加密层 http协议内容都是按照文本的方式明文传输的&#xff0c;这就导致在传输过程中出现一些被篡改的情况 http和https是可以同时存在的&#xff0c;数据时从应用层自上往下发的&#xff0c…

数仓建模—ChatETL

数仓建模—ChatETL 前面我们介绍过ChatBI ,就是让用户通过自然语言对话的方式可以获取到自己想要的数据,然后通过合适的报表展示出来,其实我们可以将其理解为应用层面的技术创新,但是这个实现的前提就是我们底层已经有加工好的大量的数据模型数据表,并且有完善的元数据建…

【Python】解决Python报错:AttributeError: ‘str‘ object has no attribute ‘xxx‘

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

每日刷题——相遇、宝石(模拟+数学)、相助(模拟+数组)、相依(dp的优化)

相遇 原题链接登录—专业IT笔试面试备考平台_牛客网 题目描述 运行代码 #include<iostream> using namespace std; int main(){ int a,b; cin>>a>>b; if(ab) { cout<<"p"; } else if(a - b 1 || (a 1 && b 3)){cout <<…

系统架构设计师【第10章】: 软件架构的演化和维护 (核心总结)

文章目录 10.1 软件架构演化和定义的关系10.1.1 演化的重要性10.1.2 演化和定义的关系 10.2 面向对象软件架构演化过程10.2.1 对象演化10.2.2 消息演化10.2.3 复合片段演化10.2.4 约束演化 10.3 软件架构演化方式的分类10.3.1 软件架构演化时期10.3.2 软件架构静态演…