Go语言context包源码剖析

context包的作用

context包是在go1.7版本中引入到标准库中的

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用backgroundTODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadlineWithTimeoutWithCancel WithValue创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

context包概述

创建context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两种方式其实并没有什么差别,只是互为别名,官方给的定义是:

  • context.Background是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO应该只在不确定应该使用哪种上下文时使用;

在大多数情况下,我们都使用context.Backgroud()作为起始的上下文传递
上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生:

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

context包源码剖析

Context抽象接口

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
  • deadline:

Deadline 返回代表此上下文完成的工作应该被取消的时间。 当没有设置截止日期时,截止日期返回 ok==false。 对 Deadline 的连续调用返回相同的结果。

  • Done:

Done 返回一个通道,当应该取消代表此上下文完成的工作时,该通道将关闭。 如果此上下文永远无法取消,则 Done 可能会返回 nil。 对 Done 的连续调用返回相同的值。

  • WithCancel 安排在调用 cancel 时关闭 Done;
  • WithDeadline 安排在截止日期到期时关闭 Done;
  • WithTimeout 安排在超时结束时关闭 Done。

提供 Done 以供在 select 语句中使用:
Stream 使用 DoSomething 生成值并将它们发送到 out,直到 DoSomething 返回错误或 ctx.Done 关闭。

  func Stream(ctx context.Context, out chan<- Value) error {
  for {
  	v, err := DoSomething(ctx)
  	if err != nil {
  		return err
  	}
  	select {
  	case <-ctx.Done():
  		return ctx.Err()
  	case out <- v:
  	}
  }
}
  • Err

如果 Done 尚未关闭,Err 返回 nil。
如果 Done 关闭,Err 返回一个非零错误来解释原因:
如果上下文被取消,则为取消; 如果上下文的最后期限已过,则为 DeadlineExceded。
在 Err 返回非零错误后,对 Err 的连续调用返回相同的错误。

这个接口主要是被是被三个类继承实现的,分别是emptyCtxValueCtxcancelCtx,采用匿名接口的写法,这样可以对任意实现了该接口的类型进行重写。

emptyCtx

emptyCtx是一个空Context,主要充当根context

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"
}

里面的各种接口,都是空实现
context.Backgroud()context.TODO(),就是两个emotyCtx

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

backgroundtodo是两个单例对象,实现方法是一模一样的

valueCtx

valueCtx目的就是为Context携带数据,他会继承父Context

type valueCtx struct {
	Context
	key, val any
}

valueCtx类的创建,下面带代码的实现看起来十分简单,就是返回一个valueCtx实例

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}
}

该类实现了String方法输出context和携带的键值对信息

func stringify(v any) string {
	switch s := v.(type) {
	case stringer:
		return s.String()
	case string:
		return s
	}
	return "<not Stringer>"
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

该类还提供获取value的Value方法

func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, 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 == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case *emptyCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

该方法的实现就是从树的最底层向上找,直到找到或者到达根Context为止,context树形结构如下
在这里插入图片描述
以ctx1.1为例,现在ctex1.1找,找不到,会往ctx1.0找,再找不到就会到达根context返回err

cancelCtx

cnacelCtx也会继承父context

type cancelCtx struct {
	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                 // set to non-nil by the first cancel call
}

  • mu:保护下面的成员
  • done:用来通知context的取消信号,采用的原子操作
  • children:存放当前接口的字节点,当根节点发生取消时,所有子节点也需要取消
  • err:用来存放取消context时的信息
  • 通过newCancelCtx方法创建cancelCtx实例对象
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

该类也实现了Valie方法,与上面类似具体细节不过多赘述

实现了done方法,返回一个单向管道,当应该取消代表此上下文完成的工作时,该通道将关闭。 如果此上下文永远无法取消,则 Done 可能会返回 nil。 对 Done 的连续调用返回相同的值。

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

propagateCancel方法用来构建父子节点之间的关系

func propagateCancel(parent Context, child canceler) {
   //如果返回的是nil说明父节点不会被取消,就不用管,直接返回就可以
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	//判断要构建关联的节点是否被取消,被取消也就不需要构建关联了,该节点直接取消
	//节点可能是timeCtx和cancelCtx
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}
  //这里的目的是去寻找可以构建关联的的context,可能是父节点,祖父节点,曾祖父节点.....
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// 构建关联的Context已经被取消,所以不需要再构建关联,子节点直接取消
			child.cancel(false, p.err)
		} else {
			//初始化children节点的childrenMap
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			//将该节点添加到父节点children当中
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
	//没有找到可以构建关联的父节点
	//那么需要开一个协程关注父节点是否被取消,父节点取消,该节点取消
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

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
	//关闭chan,通知其他协程
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	//遍历每一个children,取消所有孩子节点
	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()

	//如果为true,则从父节点中将该节点移除
	if removeFromParent {
		removeChild(c.Context, c)
	}
}

timerCtx

timerCtx基于 cancelCtx,继承了cancelCtx只是多了一个 time.Timer和一个 deadline。Timer 会在 deadline到来时,自动取消 context。

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

先看WithTimeout方法,它内部就是调用的WithDeadline方法:

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

下面看看WithDeadline

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)
	}
	//创建timerCtx实例
	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) }
}

timerCtx实现的cancel方法,内部也是调用了cancelCtxcancel方法取消:

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()
}

以上就是Context包的大致内容

context的优缺点

缺点 :

  • 让代码变得十分丑陋,可读性变差,因为我们不得不将context作为一个参数,进行传递
  • context取消和自动取消的错误返回不够友好,无法自定义错误,出现难以排查的问题时不好排查。
  • 衍生的context就像一个个链表节点,当节点过多时,会影响代码效率

优点:

  • 可以更好的,更方便的管理协程
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
ContextGo语言内置的一个标准库,主要用于多个Goroutine之间的上下文传递和控制。它提供了一种机制来传递取消信号、截止时间和一些其他的请求/值,这些请求/值可以跨越多个API边界和Goroutine传递,而不需要显式地传递。 以下是Context的主要码分析: 1. Context接口 Context接口定义了一个可取消的上下文,它括了Deadline截止时间、Done通道和Value键值对数据。 ```go type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } ``` 2. context.Background() Background函数返回一个空的Context,它没有任何值和截止时间,而且永远不会取消。它被广泛用于Main函数、初始化和测试中。 ```go func Background() Context { return background } var ( background = new(emptyCtx) ) 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 interface{}) interface{} { return nil } ``` 3. context.TODO() TODO函数返回一个非空的Context,它没有任何值和截止时间,而且永远不会取消。它被广泛用于暂时不确定上下文应该是什么的情况。 ```go func TODO() Context { return todo } var ( todo = new(emptyCtx) ) ``` 4. context.WithCancel() WithCancel函数返回一个带有CancelFunc的Context,当CancelFunc调用时,Context的Done通道将被关闭。这个函数可以用来取消长时间运行的操作。 ```go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } type cancelCtx struct { Context mu sync.Mutex // protects following fields done chan struct{} // 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 } func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} } func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d } func (c *cancelCtx) Err() error { c.mu.Lock() defer c.mu.Unlock() return c.err } 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 if c.done == nil { c.done = closedchan } else { close(c.done) } 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) } } type canceler interface { cancel(removeFromParent bool, err error) } var closedchan = make(chan struct{}) func init() { close(closedchan) } ``` 5. context.WithDeadline() WithDeadline函数返回一个带有截止时间的Context,当截止时间到达或者调用CancelFunc时,Context的Done通道将被关闭。 ```go func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: deadline, } propagateCancel(parent, c) d := c.deadline.Sub(time.Now()) if d <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(true, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(d, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } type timerCtx struct { cancelCtx deadline time.Time mu sync.Mutex // protects timer and err timer *time.Timer err error } func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) // propagate the cancel first c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.err = err c.mu.Unlock() if removeFromParent { removeChild(c.cancelCtx.Context, c) } } ``` 6. context.WithTimeout() WithTimeout函数返回一个带有超时时间的Context,当超时时间到达或者调用CancelFunc时,Context的Done通道将被关闭。 ```go func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } ``` 7. context.WithValue() WithValue函数返回一个带有键值对数据的Context,这个数据可以跨越多个API边界和Goroutine传递,而不需要显式地传递。 ```go func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if val == nil { panic("nil value") } return &valueCtx{parent, key, val} } type valueCtx struct { Context key, val interface{} } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } ``` 以上就是Context码分析。Context提供了一种简单而强大的机制来传递请求/值和取消信号,可以用于管理并发访问、超时控制和错误处理等场景。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

binary~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值