开发者

Go中Context使用源码解析

目录
  • 前言
  • 1、Context定义
  • 2、Context的派生
    • 2.1、创建Context对象
    • 2.2、parent Context
  • 3、context 接口四种实现
    • 4、 emptyCtx 源码分析
      • 5、 canchttp://www.devze.comelCtx 源码分析
        • 5.1、对象创建withCancel()
          • 5.1.1、newCancelCtx
          • 5.1.2、propagateCancel
        • 5.2 canceler
        • 6、timerCtx 源码分析
          • 6.1、对象创建 WithDeadline和WithTimeout
            • 6.2 timerCtx的cancel
            • 7、valueCtx 源码分析
              • 7.1、对象创建WithValue
                • 7.2、获取value值
                • 8、规范&注意事项

                  前言

                  本篇内容的主题是Go中Context,想必已学习Go语言的大家在熟悉不过了。工作中我们也常会用到,但有时很少去注意它。

                  本打算将相关知识点归总一下,发现其源码不多,就打算对其源码进行分析一下。

                  context包是在go1.17是引入到标准库中,且标准库中大部分接口都将context.Context作为第一个参数。

                  context中文译为“上下文”,实际代表的是goroutine的上下文。且多用于超时控制和多个goroutine间的数据传递。

                  本篇文章将带领大家深入了解其内部的工作原理。

                  1、Context定义

                  Context 接口定义如下

                  type Context interface {
                    // Deadline returns the time when this Context will be canceled, if any.
                  	Deadline() (deadline time.Time, ok bool)
                    // Done returns a channel that is closed when this Context is canceled
                    // or times out.
                  	Done() <-chan struct{}
                    // Err indicates why this context was canceled, after the Done channel
                    // is closed.
                  	Err() error
                    // Value returns the value associykFGxxated with key or nil if none.
                  	Value(key any) any
                  }
                  

                  Deadline(): 返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。

                  Done(): 返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。这里就简称信号通道吧!

                  Err():返回Context 被取消的原因

                  Value: 从Context中获取与Key关联的值,如果没有就返回nil

                  2、Context的派生

                  2.1、创建Context对象

                  context包提供了四种方法来创建context对象,具体方法如下:

                  func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
                  func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
                  func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
                  func WithValue(parent Context, key, val any) Context {}
                  

                  由以上方法可知:新的context对象都是基于父context对象衍生的.

                  • WithCancel:创建可以取消的Context
                  • WithDeadline: 创建带有截止时间的Context
                  • WithTimeout:创建带有超时时间的Context,底层调用的是WithDeadline方法
                  • WithValue:创建可以携带KV型数据的Context

                  简单的树状关系如下(实际可衍生很多中):

                  Go中Context使用源码解析

                  2.2、parent Context

                  context包默认提供了两个根context 对象backgroundtodo;看实现两者都是由emptyCtx创建的,两者的区别主要在语义上,

                  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
                  • context.TODO 应该仅在不确定应该使用哪种上下文时使用
                  var (
                  	background = new(emptyCtx)
                  	todo       = new(emptyCtx)
                  )
                  // Background 创建background context
                  func Background() Context {
                  	return background
                  }
                  // TODO 创建todo context
                  func TODO() Context {
                  	return todo
                  }
                  

                  3、context 接口四种实现

                  Go中Context使用源码解析

                  具体结构如下,我们大致看下相关结构体中包含的字段,具体字段的含义及作用将在下面分析中会提及。

                  • emptyCtx
                  type emptyCtx int // 空context
                  
                  • cancelCtx
                  type cancelCtx struct {
                  	Context // 父context
                  	mu       sync.Mutex            // protects following fields
                  	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
                  	children map[canceler]struct{} // set to nil by the first cancel call
                  	err      error                 // cancel编程客栈的原因
                  }
                  
                  • timerCtx
                  type timerCtx struct {
                  	cancelCtx //父context
                  	timer *time.Timer // 定时器
                  	deadline time.Time // 截止时间
                  }
                  
                  • valueCtx
                  type valueCtx struct {
                  	Context // 父context
                    key, val any // kv键值对
                  }
                  

                  4、 emptyCtx 源码分析

                  emptyCtx实现非常简单,具体代码如下,我们简单看看就可以了

                  // An emptyCtx is never canceled, has no values, and has no deadline. It is not
                  // struct{}, since vars of this type must have distinct addresses.
                  type emptyCtx int
                  func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
                  	return
                  }
                  func (*emptyCtx) Done() <-chan struct{} {
                  	return nil
                  }
                  func (*emptyCtx) Err() error {
                  	return nil
                  }
                  func (*emptyCtx) Value(key any) any {
                  	return nil
                  }
                  func (e *emptyCtx) String() string {
                  	switch e {
                  	case background:
                  		return "context.Background"
                  	case todo:
                  		return "context.TODO"
                  	}
                  	return "unknown empty Context"
                  }
                  

                  5、 cancelCtx 源码分析

                  cancelCtx 的实现相对复杂点,比如下面要介绍的timeCtx 底层也依赖它,所以弄懂cancelCtx的工作原理就能很好的理解context.

                  cancelCtx 不仅实现了Context接口也实现了canceler接口

                  5.1、对象创建withCancel()

                  func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
                  	if parent == nil { // 参数校验
                  		panic("cannot create context from nil parent")
                  	}
                    // cancelCtx 初始化
                  	c := newCancelCtx(parent)
                  	propagateCancel(parent, &c) // cancelCtx 父子关系维护及传播取消信号
                  	return &c, func() { c.cancel(true, Canceled) } // 返回cancelCtx对象及cannel方法
                  }
                  // newCancelCtx returns an initialized cancelCtx.
                  func newCancelCtx(parent Context) cancelCtx {
                  	return cancelCtx{Context: parent}
                  }
                  

                  用户调用WithCancel方法,传入一个父 Context(这通常是一个 background,作为根节点),返回新建的 context,并通过闭包的形式返回了一个 cancel 方法。如果想要取消context时需手动调用cancel方法。

                  5.1.1、newCancelCtx

                  cancelCtx对象初始化, 其结构如下:

                  type cancelCtx struct {
                    // 父 context
                  	Context //  parent context
                  	// 锁 并发场景下保护cancelCtx结构中字段属性的设置
                  	mu       sync.Mutex            // protects following fields 
                    // done里存储的是信号通道,其创建方式采用的是懒加载的方式
                  	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call 
                    // 记录与父子cancelCtx对象,
                  	children map[canceler]struct{} // set to nil by the first cancel call
                    // 记录ctx被取消的原因
                  	err      error                 // set to non-nil by the first cancel call
                  }
                  

                  5.1.2、propagateCancel

                  propagateCancel

                  // propagateCancel arranges for child to be canceled when parent is.
                  func propagateCancel(parent Context, child canceler) {
                  	done := parent.Done() // 获取parent ctx的信号通道 done
                  	if done == nil {  // nil 代表 parent ctx 不是canelctx 类型,不会被取消,直接返回
                  		return // parent is never canceled
                  	}
                  	select { // parent ctx 是cancelCtx类型,判断其是否被取消
                  	case <-done:
                  		// parent is already canceled
                  		child.cancel(false, parent.Err())
                  		return
                  	default:
                  	}
                    //parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的
                  	if p, ok := parentCancelCtx(parent); ok { // 查询到
                  		p.mu.Lock() // 加锁
                  		if p.err != nil { // 祖父 ctx 已经被取消了,则 子cancelCtx 也需要调用cancel 方法来取消
                  			// parent has already been canceled
                  			child.cancel(false, p.err)
                  		} else { // 使用map结构来维护 将child加入到祖父context中
                  			if p.children == nil {
                  				p.children = make(map[canceler]struct{})
                  			}
                  			p.children[child] = struct{}{}
                  		}
                  		p.mu.Unlock()// 解锁
                  	} else { // 开启协程监听 parent Ctx的取消信号 来通知child ctx 取消
                  		atomic.AddInt32(&goroutines, +1)
                  		go func() {
                  			select {
                  			case <-parent.Done():
                  				child.cancel(false, parent.Err())
                  			case <-child.Done():
                  			}
                  		}()
                  	}
                  }
                  
                  // parentCancelCtx returns the underlying *cancelCtx for parent.
                  // It does this by looking up parent.Value(&cancelCtxKey) to find
                  // the innermost enclosing *cancelCtx and then checking whether
                  // parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
                  // has been wrapped in a custom implementation providing a
                  // different done channel, in which case we should not bypass it.)
                  // parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的
                  func parentCancelCtx(parent Context) (*cancelCtx, bool) {
                  	done := parent.Done()
                    // closedchan 代表此时cancelCtx 已取消, nil 代表 ctx不是cancelCtx 类型的且不会被取消
                  	if done == closedchan || done == nil { 
                  		return nil, false
                  	}
                    // 向上遍历查询canelCtx 类型的ctx
                  	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
                  	if !ok { // 没有
                  		return nil, false
                  	}
                    // 存在判断信号通道是不是相同
                  	pdone, _ := p.done.Load().(chan struct{})
                  	if pdone != done {
                  		return nil, false
                  	}
                  	return p, true
                  }
                  

                  5.2 canceler

                  cancelCtx也实现了canceler接口,实现可以 取消上下文的功能。

                  canceler接口定义如下:

                  // A canceler is a context type that can be canceled directly. The
                  // implementations are *cancelCtx and *timerCtx.
                  type canceler interface {
                  	cancel(removeFromParent bool, err error) // 取消
                  	Done() <-chan struct{} // 只读通道,简称取消信号通道
                  }
                  

                  cancelCtx 接口实现如下:

                  整体逻辑不复杂,逻辑简化如下:

                  • 当前 cancelCtx 取消 且 与之ykFGxx其关联的子 cancelCtx 也取消
                  • 根据removeFromParent标识来判断是否将子 cancelCtx 移除

                  注意

                  由于信号通道的初始化采用的懒加载方式,所以有未初始化的情况;

                  已初始化的:调用close 函数关闭channel

                  未初始化的:用 closedchan 初始化,其closedchan是已经关闭的channel。

                  // 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
                  	d, _ := c.done.Load().(chan struct{})
                  	if d == nil {
                  		c.done.Store(closedchan)
                  	} else {
                  		close(d)
                  	}
                  	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()
                  	if removeFromParent {
                  		removeChild(c.Context, c)
                  	}
                  }
                  
                  // removeChild removes a context from its parent.
                  func removeChild(parent Context, child canceler) {
                  	p, ok := parentCancelCtx(parent)
                  	if !ok {
                  		return
                  	}
                  	p.mu.Lock()
                  	if p.children != nil {
                  		delete(p.children, child)
                  	}
                  	p.mu.Unlock()
                  }
                  

                  closedchan

                  可重用的关闭通道,该channel通道默认已关闭

                  // closedchan is a reusable closed channel.
                  var closedchan = make(chan struct{})
                  func init() {
                  	close(closedchan) // 调用close 方法关闭
                  }
                  

                  6、timerCtx 源码分析

                  cancelCtx源码已经分析完毕,那timerCtx理解起来就很容易。

                  关注点timerCtx是如何取消上下文的,以及取消上下文的方式

                  6.1、对象创建 WithDeadline和WithTimeout

                  WithTimeout 底层调用是WithDeadline 方法 ,截止时间是 now+timeout;

                  func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
                  	return WithDeadline(parent, time.Now().Add(timeout))
                  }
                  

                  WithDeadline 整体逻辑并不复杂,从源码中可分析出timerCtx取消上下文 采用两种方式 自动手动;其中自动方式采用定时器去处理,到达触发时刻,自动调用cancel方法。

                  deadline: 截止时间

                  timer *time.Timer : 定时器

                  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.
                  		return WithCancel(parent)
                  	}
                  	c := &timerCtx{
                  		cancelCtx: newCancelCtx(parent),
                  		deadline:  d,
                  	}
                  	propagateCancel(parent, c)
                  	dur := time.Until(d)
                  	if dur <= 0 {
                  		c.cancel(true, DeadlineExceeded) // deadline has already passed
                  		return c, func() { c.cancel(false, Canceled) }
                  	}
                  	c.mu.Lock()
                  	defer c.mu.Unlock()
                  	if c.err == nil {
                  		c.timer = time.AfterFunc(dur, func() {
                  			c.cancel(true, DeadlineExceeded)
                  		})
                  	}
                  	return c, func() { c.cancel(true, Canceled) }
                  }
                  
                  // A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
                  // implement Done and Err. It implements cancel by stopping its timer then
                  // delegating to cancelCtx.cancel.
                  type timerCtx struct {
                  	cancelCtx
                  	timer *time.Timer // Under编程 cancelCtx.mu.
                  	deadline time.Time
                  }
                  

                  6.2 timerCtx的cancel

                  • 调用cancelCtx的cancel 方法
                  • 根据removeFromParent标识,为true 调用removeChild 方法 从它的父cancelCtx的ch开发者_JS学习ildren中移除
                  • 关闭定时器 ,防止内存泄漏(着重点)
                  func (c *timerCtx) cancel(removeFromParent bool, err error) {
                  	c.cancelCtx.cancel(false, err)
                  	if removeFromParent {
                  		// Remove this timerCtx from its parent cancelCtx's children.
                  		removeChild(c.cancelCtx.Context, c)
                  	}
                  	c.mu.Lock()
                  	if c.timer != nil {
                  		c.timer.Stop()
                  		c.timer = nil
                  	}
                  	c.mu.Unlock()
                  }
                  

                  7、valueCtx 源码分析

                  7.1、对象创建WithValue

                  valueCtx 结构体中有keyval两个字段,WithValue 方法也是将数据存放在该字段上

                  func WithValue(parent Context, key, val any) Context {
                  	if parent == nil {
                  		panic("cannot create context from nil parent")
                  	}
                  	if key == nil {
                  		panic("nil key")
                  	}
                  	if !reflectlite.TypeOf(key).Comparable() {
                  		panic("key is not comparable")
                  	}
                  	return &valueCtx{parent, key, val}
                  }
                  // A valueCtx carries a key-value pair. It implements Value for that key and
                  // delegates all other calls to the embedded Context.
                  type valueCtx struct {
                  	Context
                  	key, val any
                  }
                  

                  7.2、获取value值

                  func (c *valueCtx) Value(key any) any {
                  	if c.key == key { // 判断当前valuectx对象中的key是否匹配
                  		return c.val
                  	}
                  	return value(c.Context, key)
                  }
                  // value() 向根部方向遍历,直到找到与key对应的值
                  func value(c Context, key any) any {
                  	for {
                  		switch ctx := c.(type) {
                  		case *valueCtx:
                  			if key == ctx.key {
                  				return ctx.val
                  			}
                  			c = ctx.Context
                  		case *cancelCtx:
                  			if key == &amp;cancelCtxKey { // 获取cancelCtx对象
                  				return c
                  			}
                  			c = ctx.Context
                  		case *timerCtx:
                  			if key == &amp;cancelCtxKey {
                  				return &amp;ctx.cancelCtx
                  			}
                  			c = ctx.Context
                  		case *emptyCtx:
                  			return nil
                  		default:
                  			return c.Value(key)
                  		}
                  	}
                  }
                  

                  总结:从Context 中获取对应的值需要通过遍历的方式来获取,这里告诫我们嵌套太多的context反而对性能会有影响

                  8、规范&注意事项

                  • 不要把context存在一个结构体当中,显式地传入函数。context变量需要作为第一个参数使用,一般命名为ctx;
                  • 即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO
                  • 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
                  • 同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的

                  以上就是Go中Context使用源码解析的详细内容,更多关于Go Context源码解析的资料请关注我们其它相关文章!

                  0

                  上一篇:

                  下一篇:

                  精彩评论

                  暂无评论...
                  验证码 换一张
                  取 消

                  最新开发

                  开发排行榜