设为首页 加入收藏

TOP

Go语言 context包源码学习(五)
2023-07-23 13:25:51 】 浏览:117
Tags:语言 context 包源码
nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // parent 不是 cancelCtx 类型,可能是用户自己实现的Context atomic.AddInt32(&goroutines, +1) // 启动一个协程监听,如果 parent 取消了,子 Context 也取消 go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }

cancel 方法就是来取消 cancelCtx,主要的工作是:关闭c.done 中的channel,给 err 赋值,然后级联取消所有 子Context。如果 removeFromParent 为 true,会从父节点中删除以该节点为树顶的树。

cancel() 方法只负责自己管辖的范围,即自己以及自己的子节点,然后根据配置判断是否需要从父节点中移除自己为顶点的树。如果子节点还有子节点,那么由子节点负责处理,不用自己负责了。

propagateCancel() 中有三处调用了 cancel() 方法,传入的 removeFromParent 都为 false,是因为当时根本没有挂载,不需要移除。而 WithCancel 返回的 CancelFunc ,传入的 removeFromParent 为 true,是因为调用 propagateCancel 有可能产生挂载,当产生挂载时,调用 cancel() 就需要移除了。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  
  // err 是指取消的原因,必传,cancelCtx 中是 errors.New("context canceled")
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
  
  // 涉及到保护字段值的修改,都需要加锁
	c.mu.Lock()
  
  // 如果该Context已经取消过了,直接返回。多次调用cancel,不会产生额外效果
	if c.err != nil {
		c.mu.Unlock()
		return 
	}
  
  // 给 err 赋值,这里 err 一定不为 nil
	c.err = err
  
  // close channel
	d, _ := c.done.Load().(chan struct{})
  // 因为c.done 是懒加载,有可能存在 nil 的情况
  // 如果 c.done 中没有值,直接赋值 closedchan;否则直接 close
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
  
  // 遍历当前 cancelCtx 所有的子Context,让子节点也 cancel
  // 因为当前的Context 会主动把子Context移除,子Context 不用主动从parent中脱离
  // 因此 child.cancel 传入的 removeFromParent 为false
	for child := range c.children {
		child.cancel(false, err)
	}
  // 将 children 置空,相当于移除自己的所有子Context
	c.children = nil
	c.mu.Unlock()
	
  // 如果当前 cancelCtx 需要从上层的 cancelCtx移除,调用removeChild方法
  // c.Context 就是自己的父Context
	if removeFromParent {
		removeChild(c.Context, c)
	}
}

从propagateCancel方法中可以看到,只有parent 属于 cancelCtx 类型 ,才会将自己挂载。因此 removeChild 会再次判断 parent 是否为 cancelCtx,和之前的逻辑保持一致。找到的话,再将自己移除,需要注意的是,移除会把自己及其自己下面的所有子节点都移除。

如果上一步 propagateCancel 方法将自己挂载到了 A 上,但是在调用 cancel() 时,A 已经取消过了,此时 parentCancelCtx() 会返回 false。不过这没有关系,A 取消时已经将挂载的子节点移除了,当前的子节点不用将自己从 A 中移除了。

func removeChild(parent Context, child canceler) {
  // parent 是否为未取消的 cancelCtx
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
  // 获取 parent cancelCtx 的锁,修改保护字段 children
	p.mu.Lock()
  // 将自己从 parent cancelCtx 的 children 中删除
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

parentCancelCtx 判断 parent 是否为 未取消的 *cancelCtx。取消与否容易判断,难判断的是 parent 是否为 *cancelCtx,因为有可能其他结构体内嵌了 cancelCtx,比如 timerCtx,会通过比对 channel 来确定。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
  
  // 如果 parent context 的 done 为 nil, 说明不支持 cancel,那么就不可能是 cancelCtx
	// 如果 parent context 的 done 为 closedchan, 说明 parent context 已经 cancel 了
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
  
  // 到这里说明支持取消,且没有被取消
  
	// 如果 parent context 属于原生的 *cancelCtx 或衍生类型,需要继续进行后续判断
	// 如果 parent context 无法转换到 *cancelCtx,则认为非 cancelCtx,返回 nil,fasle
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
 
  // 经过上面的判断后,说明 parent context 可以被转换为 *cancelCtx,这时存在多种情况:
	//   - parent context 就是 *cancelCtx
	//   - parent context 是标准库中的 timerCtx
	//   - parent context 是个自己自定义包装的 cancelCtx
	//
	// 针对这 3 种情况需要进行判断,判断方法就是: 
	//   判断 parent context 通过 Done
首页 上一页 2 3 4 5 6 下一页 尾页 5/6/6
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇为什么说 Go 语言字符串是不可变.. 下一篇[grpc快速入门] 一 grpc生成与调用

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目