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 实现多变组合。
学习组合增强的方式,在不改变原逻辑的前提下,增强能力。
相关文章:

Go-知识并发控制Context
Go-知识并发控制Context 1. 介绍2. 实现原理2.1 接口定义2.2 Deadline()2.3 Done()2.4 Err()2.5 Value() 3. 空 context4. cancelCtx4.1 Done()4.2 Err()4.3 cancel()4.4 WithCancel4.5 例子4.6 总结 5. timerCtx5.1 Deadline5.2 cancel5.3 WithDeadline5.4 WithTimeout5.5 例子…...
Vue + Nodejs + socket.io 实现聊天
Vue 代码 // 安装 socket.io-clientnpm i socket.io-clientimport io from socket.io-client;mounted () {// * location.origin 表示你的 socket 服务地址// * /XXXX/socket.io 表示 你的 socket 在服务器配置的 访问地址let socket io(location.origin, {path: "/XX…...

cocos creator 3.x实现手机虚拟操作杆
简介 在许多移动游戏中,虚拟操纵杆是一个重要的用户界面元素,用于控制角色或物体的移动。本文将介绍如何在Unity中实现虚拟操纵杆,提供了一段用于移动控制的代码。我们将讨论不同类型的虚拟操纵杆,如固定和跟随,以及如…...

【数据分享】中国电力年鉴(2004-2022)
大家好!今天我要向大家介绍一份重要的中国电力统计数据资源——《中国电力年鉴》。这份年鉴涵盖了从2004年到2022年中国电力统计全面数据,并提供限时免费下载。(无需分享朋友圈即可获取) 数据介绍 自1993年首次出版以来…...
两个数组的交集Ⅱ-力扣
想到的解法是使用两个map来进行记录,mp1用来统计num1中每个元素出现的次数。当nums2的元素能够在mp1中查找到时,将这个元素添加到mp2,按照这个规则统计得到nums2和nums1重复的元素,mp2中的value记录了nums2中这个元素出现的次数最…...

【TCP协议中104解析】wireshark抓取流量包工具,群殴协议解析基础
Tcp ,104 ,wireshark工具进行解析 IEC104 是用于监控和诊断工业控制网络的一种标准,而 Wireshark则是一款常用的网络协议分析工具,可以用干解析TEC104 报文。本文将介绍如何使用 Wireshark解析 IEC104报文,以及解析过 程中的注意事项。 一、安…...

[个人笔记] 记录docker-compose使用和Harbor的部署过程
容器技术 第三章 记录docker-compose使用和Harbor的部署过程 容器技术记录docker-compose使用和Harbor的部署过程Harborhttps方式部署:测试环境部署使用自签名SSL证书https方式部署:正式环境部署使用企业颁发的SSL证书给Docker守护进程添加Harbor的SSL证…...

详细介绍运算符重载函数,清晰明了
祝各位六一快乐~ 前言 1.为什么要进行运算符重载? C中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作。这时就必须在C中重新定义这些运算符ÿ…...
国内外知名的低代码开发平台下载地址
以下是国内外几款低代码开发平台的列表,包含了下载地址、适应操作系统、是否可以独立部署、优点、缺点以及是否包含流程引擎的信息。 平台名称 下载地址 适应操作系统 是否可以独立部署 优点 缺点 是否包含流程引擎 国内平台 阿里云宜搭 阿里云官网 跨平台…...

【Pr学习】01新建项目起步
【Pr学习】01新建项目起步 1、新建项目2.序列设置2.1新建序列2.2序列参数讲解2.3自定义设置 3.PR窗口认识3.1 项目窗口3.2 源窗口2.4 保存面板 4.剪辑导入4.1 素材导入4.2 视图切换4.3 时间轴4.4轨道工具4.5 节目窗口素材导入 5.基础操作5.1 取消视频音频链接5.2 单独渲染&…...
【Redis延迟队列】redis中的阻塞队列和延迟队列
阻塞队列(RBlockingQueue) 作用和特点: 实时性:阻塞队列用于实时处理消息。生产者将消息放入队列,消费者可以立即从队列中取出并处理消息。阻塞特性:如果队列为空,消费者在尝试获取消息时会被…...
el-tree常用操作
一、定义 <el-treeclass"myTreeClass":data"dirTreeData":props"dirTreeProps":filter-node-method"filterDirTree":expand-on-click-node"false"node-key"id"node-click"dirTreeNodeClick":allow-…...
SQL 语言:存储过程和触发器
文章目录 基本概述创建触发器更改和删除触发器总结 基本概述 存储过程,类似于高阶语言的函数或者方法,包含SQL语句序列,是可复用的语句,保存在数据库中,在服务器中执行。特点是复用,提高了效率,…...
Ubuntu Linux 24.04 使用certbot生成ssl证书
设置域名 1. 将需要生成SSL证书的域名解析到IP地址 idealand.xyz <> 64.176.82.190 检查防火墙的设置 1. 首先查看防火墙的状态: # ufw status 2. 如果防火墙开启了,要开放80和443端口用于certbot验证 # ufw allow 80 # ufw allow 443 生…...

Vivado 比特流编译时间获取以及FPGA电压温度获取(实用)
Vivado 比特流编译时间获取以及FPGA电压温度获取 语言 :Verilg HDL 、VHDL EDA工具:ISE、Vivado Vivado 比特流编译时间获取以及FPGA电压温度获取一、引言二、 获取FPGA 当前程序的编译时间verilog中直接调用下面源语2. FPGA电压温度获取(1&a…...

Window下VS2019编译WebRTC通关版
这段时间需要实现这样一个功能,使用WebRTC实现语音通话功能,第一步要做的事情就是编译WebRTC源码,也是很多码友会遇到的问题。 经过我很多天的踩坑终于踩出来一条通往胜利的大路,下面就为大家详细介绍,编译步骤以及踩…...

【云原生 | 60】Docker中通过docker-compose部署kafka集群
🍁博主简介: 🏅云计算领域优质创作者 🏅2022年CSDN新星计划python赛道第一名 🏅2022年CSDN原力计划优质作者 🏅阿里云ACE认证高级工程师 🏅阿里云开发者社区专…...

allure测试报告用例数和 pytest执行用例数不相同问题
我出现的奇怪问题: pytest执行了9条用例,但是测试报告确只显示3条用例 我将其中的一个代码删除后,发现allure测试报告又正常了 我觉得很奇怪这个代码只是删除了二维数组的第一列,我检查了半天都找不到问题,只有降低版本…...
Ubuntu 离线安装 gcc、g++、make 等依赖包
前言 项目现场的服务器无法连接互联网,需要提前获取 gcc、g、make 等依赖包。 一、如何获取依赖包 需要准备一台可以连接互联网的电脑(如:个人电脑上的虚拟机安装一个与服务器一样的系统),用于下载依赖包。之后把通过…...

Vxe UI vxe-upload 上传组件,显示进度条的方法
vxe-upload 上传组件 查看官网 https://vxeui.com 显示进度条很简单,需要后台支持进度就可以了,后台实现逻辑具体可以百度,这里只介绍前端逻辑。 上传附件 相关参数说明,具体可以看文档: multiple 是否允许多选 li…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...

【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...

MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...

抽象类和接口(全)
一、抽象类 1.概念:如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象,这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法,包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中,⼀个类如果被 abs…...

密码学基础——SM4算法
博客主页:christine-rr-CSDN博客 专栏主页:密码学 📌 【今日更新】📌 对称密码算法——SM4 目录 一、国密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特点 2.3 基本部件 2.3.1 S盒 2.3.2 非线性变换 编辑…...