您的位置 首页 golang

Context包中用于通知的通道延迟初始化分析

关于context

context包在golang中具有十分重要的地位,但凡写过go代码或者使用过go任何一款框架的都会在很多地方看到方法传参中的context.Context。

其作用,主要有以下两点:

  1. 管理通知子协程
  2. 在协程中进行参数传递

通过分析源码 (源码位置:context/context.go ) 发现,其主要由以下几部分组成:

  1. Context接口
  2. 4个主要的包内私有struct
  3. 6个包级别的对外方法

context

通知通道的延迟初始化分析

在阅读源码时发现一个有趣的地方,通知子协程的通道竟然是在使用的时候进行的初始化,下面通过一个demo来追溯其原理。

 // Context Demo
func ContextDemo() {
    // Context底层源码位置:context/context.go 
    // 创建context根节点,其具体类型为emptyCtx
    rootCtx := context.Background()

    // context.WithCancel生成一个带有cancel动作的子节点ctx(ctx中包裹着根节点),其具体类型为cancelCtx    
    ctx, cancelFunc := context.WithCancel(rootCtx)

    gofunc(ctx context.Context) {
        // do something

        // 在子协程中监听 ctx节点是否执行了cancel动作      
        select {
	     case <-ctx.Done(): //ctx.Done() 为一个只读通道
		fmt.Println("parent cancel")
	     return
	}
    }(ctx)

    //执行cancel动作
    cancelFunc()
}  

关于通过context.WithCancel(rootCtx)获取子ctx的代码追溯

 // WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent) // 生成一个具体类型为cancelCtx的Context
	propagateCancel(parent, &c) // 主要是处理cancelCtx的children字段,用于记录子ctx节点
	return &c, func() { c.cancel(true, Canceled) }
}


// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     chanstruct{}         // created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

// 生成一个具体类型为cancelCtx的Context对象
// 此时发现该对象没有生成同于通知的通道 done
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}  

在通过context.WithCancel(rootCtx)获取子ctx的代码追溯中发现,其用于通知的通道done没有进行初始化。

这里先说明一下其初始化结论:

  1. 执行cancel时 初始化
  2. 在子协程中监听通道时[ctx.Done()] 初始化

至于正确与否,就通过追溯源码,来一起验证吧。

 // 1. 执行cancel动作时调用的底层方法

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock() // 先加锁,因为存在并发
	if c.err != nil {
		c.mu.Unlock()
		return// already canceled
	}
	c.err = err
  
  // !!!!重点!!!!
  // 当判断c.done为nil,也就是上面说的 用于通知的通道done为nil时,则进行赋值。否则,关闭通道以通知各个子ctx
	if c.done == nil {
    // closedchan是个奇特的存在。是在包内 init()方法中 已经执行关闭的通道
    // 也就是说c.done被赋值为一个已经关闭的通道 (因为这里是cancel操作呀)
    // 
    // closedchan 相当于下面两步:
    // c.done = make(chan struct{});close(c.done)
		c.done = closedchan
	} else {
		close(c.done)
	}
  
  // 通知各个子ctx进行关闭操作
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

  // 移除子ctx节点
	if removeFromParent {
		removeChild(c.Context, c)
	}
}


// 2. 在子协程中监听通道时[ 执行 <-ctx.Done() 方法] 初始化
func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock() // 先加锁
	if c.done == nil { // 通道为nil
		c.done = make(chanstruct{}) // !!!重要!!!!通道初始化
	}
	d := c.done
	c.mu.Unlock()
	return d
}  

总结

  1. Context父节点在通知子节点时,是通过通道cancelCtx.done来进行的
  2. 通道cancelCtx.done初始化是延迟处理的

文章来源:智云一二三科技

文章标题:Context包中用于通知的通道延迟初始化分析

文章地址:https://www.zhihuclub.com/98998.shtml

关于作者: 智云科技

热门文章

网站地图