Golang Context详解

文章目录

  • 基本介绍
  • context源码剖析
    • Context接口
    • emptyCtx
    • cancelCtx
    • timerCtx
    • valueCtx
  • context使用案例
    • 协程取消
    • 超时控制
    • 数据共享

基本介绍

基本介绍

  • 在Go 1.7版本中引入了上下文(context)包,用于在并发编程中管理请求范围的数据、控制生命周期、处理取消信号和超时等。
  • context在Go中具有重要的作用,特别是在并发编程和网络编程中,因此context通常会作为各个函数和方法的首个入参。

context源码剖析

Context接口

Context接口

Context是context包中的一个接口类型,该接口提供了对上下文的基本操作和属性的访问方法,其定义如下:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

Context接口中各方法说明:

  • Deadline方法:返回上下文的截止时间。其第一个返回值表示上下文的截止时间,第二个返回值表示上下文是否存在截止时间。
  • Done方法:返回一个只读的channel,用于接收上下文的取消信号。当上下文被取消时,该channel将会被关闭,从而通知使用者上下文已经被取消。
  • Err方法:返回与上下文关联的错误。当上下文被取消时,返回context.Canceled错误,当上下文到达截止时间时,返回context.DeadlineExceeded错误。
  • Value方法:根据指定的键获取上下文中关联的值。如果找到与键相关的值,则返回该值,如果未找到,则返回nil。

说明一下:

  • Done方法返回的channel的类型是chan struct{},而空struct中实际无法存储任何数据,因为该channel本就不是用作数据存储的,而是用作传递取消信号的。
  • 在context包中,有四个结构体类型实现了Context接口,分别是emptyCtx、cancelCtx、timerCtx和valueCtx。

emptyCtx

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
}

emptyCtx实现的四个方法说明:

  • Deadline方法:由于emptyCtx不具有截止时间,因此Deadline方法直接返回time.Time和bool类型的零值(false),表示当前上下文不存在截止时间。
  • Done方法:由于emptyCtx永远不会被取消,因此Done方法直接将nil作为只读的channel进行返回,使得该channel的读取方无法从中读取到任何取消信号。
  • Err方法:由于Err方法返回上下文被取消的原因,而emptyCtx永远不会被取消,因此Err方法直接返回nil。
  • Value方法:由于emptyCtx中不存储任何键值对,因此Value方法直接返回nil。

emptyCtx通常作为默认的顶级上下文使用,表示一个空的上下文,其他上下文类型可以在此基础上添加对应的功能,因此context树的根context一定是emptyCtx。示意图如下:

在这里插入图片描述

说明一下: 除了emptyCtx以外,其他context都是在已有context的基础上创建的。

创建emptyCtx

通过context包中的Background函数和TODO函数可以创建emptyCtx,其对应的源码如下:

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

说明一下:

  • context包中的Background和TODO函数,返回的都是各自全局复用的emptyCtx类型的实例,它们仅仅在语义上稍有不同。
  • Background函数返回的emptyCtx通常作为默认的顶级上下文使用,表示一个空的上下文,其他上下文类型可以在此基础上添加对应的功能。TODO函数返回的emptyCtx通常作为临时占位的上下文使用,表示该上下文后期需要替换为其他的上下文,目前先用emptyCtx进行占位。

cancelCtx

cancelCtx

cancelCtx是context包中的一个结构体类型,用于传播取消信号和管理取消操作,它实现了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
	cause    error                 // set to non-nil by the first cancel call
}

cancelCtx结构体各字段说明:

  • Context:嵌套的Context接口类型的匿名结构体,表示当前cancelCtx所继承的父上下文。
  • mu:互斥锁,用于保护cancelCtx结构体中各个字段的并发访问。
  • done:原子值,用于存储通知上下文已取消的channel。
  • children:子context集合,用于保存当前context的子context。
  • err:用于表示上下文被取消的错误原因。
  • cause:用于表示上下文被取消的具体原因。

children字段的类型为map[canceler]struct{},这里的map中value的类型为空struct,表示我们只关心children中是否存在某一个key,而并不关心这个key对应的value。而map中key的类型为canceler,这是context包中的一个不可导出的接口类型,用于表示可以被取消的上下文,其定义如下:

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

canceler接口中各方法说明:

  • cancel方法:用于执行上下文的取消操作,其中removeFromParent参数表示是否将当前上下文从其父上下文中移除,err表示取消的错误原因,cause表示取消的具体原因。
  • Done方法:返回一个只读的channel,用于接收上下文的取消信号。当上下文被取消时,该channel将会被关闭,从而通知使用者上下文已经被取消。

说明一下:

  • cancelCtx结构体中的children字段在保存当前context的子context时,map中的key没有直接使用Context接口类型,而是使用的canceler接口类型,因为children字段只需要关注上下文的cancel和Done这两个方法。
  • 通过定义新的接口,将对象中需要关注的能力暴露出来,同时将无关的细节屏蔽掉。这种做法有助于减少错误风险,体现了编程过程中职责内聚和边界分明的思想,同时提高了代码的可读性和可维护性。

创建cancelCtx

通过context包中的WithCancel函数和WithCancelCause函数可以创建cancelCtx,其对应的源码如下:

type CancelFunc func()
type CancelCauseFunc func(cause error)

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
	// 3、返回创建的cancelCtx和对应的取消回调函数
	return c, func() { c.cancel(true, Canceled, nil) }
}

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
	c := withCancel(parent)
	// 3、返回创建的cancelCtx和对应的取消回调函数
	return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 1、创建cancelCtx实例,并用parent对其Context字段进行初始化
	c := newCancelCtx(parent)
	// 2、将创建的cancelCtx与parent关联起来
	propagateCancel(parent, c)
	return c
}

func newCancelCtx(parent Context) *cancelCtx {
	return &cancelCtx{Context: parent} // 初始化cancelCtx的父context字段
}

创建cancelCtx的流程如下:

  1. 创建一个cancelCtx实例,并用给定的父context对cancelCtx的Context字段进行初始化。
  2. 调用propagateCancel函数将创建的cancelCtx与其父context关联起来,保证父context被取消时,子context也会被取消。
  3. 返回创建的cancelCtx,同时返回一个闭包函数,闭包函数内部通过调用cancelCtx的cancel方法执行上下文的取消操作。

说明一下:

  • WithCancel函数和WithCancelCause函数的区别在于,WithCancelCause函数返回的闭包函数在调用时支持传入cause,在取消上下文时用于设置cancelCtx的cause字段,而WithCancel函数返回的闭包函数在调用时默认cause为nil。

propagateCancel函数

propagateCancel是context包中的一个函数,用于将父context和子context关联起来,保证父context被取消时,子context也会被取消,实现取消信号的传播。propagateCancel函数对应的源码如下:

func propagateCancel(parent Context, child canceler) {
	// 1、如果parent不可取消,则直接返回
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	// 2、如果parent已经被取消,则将child也取消后返回
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		// 3、如果从parent中获取cancelCtx成功,则将child添加到parent的children集合中
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err, p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		// 4、如果从parent中获取cancelCtx失败,则启动一个协程监听parent和child的Done通道
		goroutines.Add(1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err(), Cause(parent))
			case <-child.Done():
			}
		}()
	}
}

propagateCancel函数的执行逻辑如下:

  1. 如果父context的Done方法返回的channel为nil,表明父context永远不会被取消,不必实现取消信号的传播,函数直接返回。
  2. 尝试从父context的Done通道中读取取消信号,如果读取成功则说明父context已经被取消,这时直接将子context也取消即可。
  3. 如果从父context中获取cancelCtx成功,并且此时父context没有被取消,则将子context添加到父context的children集合中。
  4. 如果从父context中获取cancelCtx失败,则启动一个协程阻塞监听父context和子context的Done通道,直到父context和子context中有一个被取消,如果监听到子context被取消则协程直接退出,如果监听到父context被取消则先将子context取消然后再退出。

说明一下:

  • 可被取消的context也可由用户自定义实现,因此从可被取消的context中获取cancelCtx可能会失败。

Deadline方法

  • cancelCtx主要用于实现取消上下文的功能,而不涉及截止时间的管理,因此cancelCtx本身并没有实现Deadline方法。
  • 在调用cancelCtx的Deadline方法时,由于cancelCtx没有实现Deadline方法,这时会调用到其父context的Deadline方法,如果其父context仍然没有实现Deadline方法,那么会继续沿着context树往上查看其各个祖先context是否实现了Deadline方法,如果其祖先context都没有实现Deadline方法,那么最终会调用到根context,即emptyCtx的Deadline方法。

Done方法

cancelCtx的Done方法会将cancelCtx中done字段存储的channel进行返回,其对应的源码如下:

func (c *cancelCtx) Done() <-chan struct{} {
	// 1、如果cancelCtx的done字段不为nil,则直接返回
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	// 2、如果cancelCtx的done字段为nil,则make后返回(双检查加锁)
	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{})
}

Done方法的执行逻辑如下:

  1. 如果cancelCtx的done字段不为nil,表明其对应的channel已经创建过了,直接将其返回即可。
  2. 如果cancelCtx的done字段为nil,则需要先通过make的方式创建对应的channel,然后再将其返回。

说明一下:

  • 从cancelCtx的Done方法的实现可以看出,cancelCtx中的Done通道采用的是懒加载机制,并在创建Done通道的过程中通过双检查加锁的方式,避免后续调用Done方法时频繁的加锁解锁操作。

Err方法

cancelCtx的Err方法会将cancelCtx中err字段的值进行返回,其对应的源码如下:

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

Value方法

cancelCtx的Value方法会根据指定的key获取上下文中关联的value,其对应的源码如下:

var cancelCtxKey int

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

Value方法的执行逻辑如下:

  1. 如果指定的key为cancelCtxKey的地址,则返回cancelCtx本身。
  2. 如果指定的key为其他值,则进一步调用value函数依次在其祖父context中查找给定key对应的value值

说明一下:

  • cancelCtxKey是context包中全局复用的一个int类型的变量,当调用cancelCtx的Value方法时,如果传入的是cancelCtxKey变量的地址,表明用户希望获取cancelCtx本身,这是context包中的一种约定。
  • cancelCtxKey约定需要用户在调用cancelCtx的Value方法时,传入cancelCtxKey变量的地址,而cancelCtxKey是context包中的一个不可导出的变量,因此该约定并不是提供给外部用户的,而是在context包内部使用的。

前面说到,在propagateCancel函数内部需要通过调用parentCancelCtx函数,从一个context中获取cancelCtx,而cancelCtxKey约定实际就是为parentCancelCtx函数定制的,该函数对应的源码如下:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	// 1、如果context的Done通道为nil或已经关闭,则获取cancelCtx失败
	done := parent.Done()
	if done == closedchan || done == nil { 
		return nil, false
	}
	// 2、如果调用Value方法(cancelCtxKey约定)从context中获取cancelCtx失败,则失败返回
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	// 3、如果获取的cancelCtx的Done通道与context的Done通道不一致,则失败返回
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	// 4、返回获取到的cancelCtx
	return p, true
}

parentCancelCtx函数的执行逻辑如下:

  1. 如果context的Done通道为nil或closedChan,表明该context不支持取消操作或已经完成了取消,获取cancelCtx失败,直接返回。
  2. 调用context的Value方法并传入cancelCtxKey的地址,如果获取cancelCtx失败,则直接返回。
  3. 如果获取从context中获取cancelCtx成功,但cancelCtx的Done通道与context的Done通道不一致,表明cancelCtx已经发生了变化,此时获取cancelCtx失败返回。
  4. 如果前面的所有检查都通过,则将获取到的cancelCtx返回。

说明一下:

  • 调用cancelCtx的Value方法时,如果没有用到cancelCtxKey约定,则会进一步调用value函数在context中获取所给key对应的value值,具体的获取过程在下面的valueCtx中进行介绍。

cancel方法

cancelCtx的cancel方法用于取消当前上下文,其对应的源码如下:

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	// 1、如果cancelCtx的err字段不为nil,表明上下文已经被取消,无需重复取消,直接返回
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	// 2、如果cancelCtx未被取消,则设置其err和cause字段,并关闭其Done通道
	c.err = err
	c.cause = cause
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	// 3、遍历cancelCtx的children集合,依次取消各个子context,并将children集合设置为nil
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err, cause)
	}
	c.children = nil
	c.mu.Unlock()

	// 4、如果removeFromParent参数为true,则将当前context从其父context中移除
	if removeFromParent {
		removeChild(c.Context, c)
	}
}

cancel方法的执行逻辑如下:

  1. 如果cancelCtx的err字段不为nil,表明上下文已经被取消,无需重复取消,直接返回。
  2. 如果cancelCtx未被取消,则设置其err和cause字段,表明上下文已经被取消,同时关闭其Done通道,使得监听Done通道的相关协程能够得知该context已经被取消。
  3. 遍历cancelCtx的children集合,依次取消各个子context,并将children集合设置为nil,实现取消信号的传播。
  4. 如果removeFromParent参数为true,则将当前context从其父context中移除。

说明一下:

  • cancel方法在关闭cancelCtx的Done通道时,如果其对应的Done通道为nil,表明该cancelCtx的Done通道还未通过make创建,这时将其Done通道设置为closedChan即可。closedChan是context包中全局复用的chan struct{}类型的变量,其通过make的方式分配了空间,并在context包的init函数中对closedChan进行了关闭,因此可以用closedChan表示一个已关闭的channel。
  • cancel方法的第一个参数removeFromParent,表示在执行取消操作时,是否将当前context从其父context中移除,移除的逻辑通过调用removeChild函数完成,实际就是将当前context从其父context的children集合中删除。
  • 并不是任何情况下,都需要将被取消的context从其父context中移除,比如cancelCtx的cancel方法在取消当前上下文时,会遍历children集合依次取消各个子context,在取消各个子context时不必将removeFromParent设置为true,因为cancel方法内部在取消完各个子context后会直接将children字段置为nil。
  • cancelCtx的cancel方法有两种情况会被调用,第一种是用户通过调用创建cancelCtx时获得的闭包函数执行上下文的取消操作,第二种是cancelCtx的父context被取消时,会遍历children集合依次调用各个子context的cancel方法执行上下文的取消操作。

协程与context

协程与context类似,协程的并发调用链路所形成的数据结构也是一个树型结构,其中协程树的根协程就是主协程,而其他协程则是程序在运行过程中创建出来的新协程。示意图如下:

在这里插入图片描述

通过将协程与context关联起来,可以实现协程的取消。具体操作如下:

  1. 仅在主协程中通过Background函数创建一个emptyCtx,保证全局只有一个context树,并在每次启动新协程的时候,通过协程的启动函数将当前的context传递给各个新协程。
  2. 如果一个协程在某些情况下需要被取消,则在该协程中通过WithCancel函数在已有context的基础上创建一个带有取消功能的cancelCtx,并将其传递给该协程创建的所有新协程,使得该协程创建的所有新协程所持有的context都带有取消能力。
  3. 当用户调用创建cancelCtx时获得的闭包函数,执行context的取消操作时,这个取消事件会从当前context处开始,依次向其子孙context传播,最终导致其所有的子孙context都被取消,即当前协程所创建的各个新协程所持有的context都会被取消。
  4. 各个协程在执行过程中,通过select的方式监听context的Done通道,可以判断各自所持有的context是否被取消,如果未被取消则执行自己的代码逻辑,如果自己所持有的context已被取消,则结束协程的运行,通过这种方式即可保证一个协程被取消时,由该协程创建的各个子孙协程都会被取消。
小贴士:在进行并发编程时要做好并发控制,避免协程泄露,如果你在创建一个协程时并不知道该协程什么时候会终止,那么你就不应该创建它。

timerCtx

timerCtx

timerCtx是context包中的一个结构体类型,其继承了cancelCtx的功能并新增了截止时间的管理功能,它实现了Context接口,其定义如下:

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

	deadline time.Time
}

timerCtx结构体各字段说明:

  • cancelCtx:嵌套的cancelCtx类型的匿名结构体指针,用于继承cancelCtx的功能。
  • timer:定时器,用于在截止时间到达时触发上下文的取消操作。
  • deadline:用于表示上下文的截止时间。

说明一下:

  • timerCtx通过嵌套cancelCtx类型的匿名结构体指针的方式,继承了cancelCtx的各个字段和方法,因此timerCtx所继承的父context,是通过cancelCtx中的Context字段来表示的,timerCtx中各个字段的并发安全,是通过cancelCtx中的mu字段来保证的。
  • 由于timerCtx继承了cancelCtx的功能,因此当timerCtx到达截止时间触发上下文的取消操作时,这个取消事件也会从当前timerCtx处开始,依次向其子孙context传播,最终导致其所有的子孙context都被取消。

创建timerCtx

通过context包中的WithDeadline函数和WithTimeout函数可以创建timerCtx,其对应的源码如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 1、如果parent存在截止时间,并且其截止时间比d早,则创建一个cancelCtx返回
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	// 2、创建timerCtx实例,并用parent和d分别对其Context字段和deadline字段进行初始化
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	// 3、将创建的timerCtx与parent关联起来
	propagateCancel(parent, c)
	// 4、如果timerCtx的截止时间在当前时间之前,则调用cancel方法对其进行取消后返回
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	// 5、创建timerCtx的定时器timer字段,使其在截止时间时间到达时执行timerCtx的取消操作
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, nil)
		})
	}
	// 6、返回创建的timerCtx和对应的取消回调函数
	return c, func() { c.cancel(true, Canceled, nil) }
}

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

创建timerCtx的流程如下:

  1. 如果给定的父context存在截止时间,并且其截止时间比子context的截止时间早,那么子context没有必要对截止时间进行管理,因为父context会在子context的截止时间到达之前被取消,这时子context也会被连带取消,因此创建一个cancelCtx返回即可。
  2. 如果给定的父context不存在截止时间,或其截止时间比子context的截止时间晚,这时子context需需要创建为timerCtx。在创建timerCtx时,分别根据所给的父context和截止时间,对timerCtx的Context字段和deadline字段进行初始化。
  3. 调用propagateCancel函数将创建的timerCtx与parent关联起来,保证父context被取消时,子context也会被取消。
  4. 如果timerCtx的截止时间在当前时间之前,则调用timerCtx的cancel方法对其进行取消后返回。
  5. 如果timerCtx的截止时间还未到达,则创建timerCtx的定时器timer字段,使其在截止时间时间到达时执行timerCtx的取消操作。
  6. 返回创建的timerCtx,同时返回一个闭包函数,闭包函数内部通过调用timerCtx的cancel方法执行上下文的取消操作。

说明一下:

  • timerCtx将会在截止时间到达时自动触发上下文的取消操作,也可以在timerCtx的截止时间到达之前,通过调用创建timerCtx时获得的闭包函数手动执行上下文的取消操作。
  • WithTimeout函数和WithDeadline函数的区别在于,WithDeadline函数使用绝对时间来指定timerCtx的截止时间,而WithTimeout函数使用的是相对时间,即从当前时间开始多久后到达截止时间。WithTimeout函数在实现时,会将传入的相对时间转换为绝对时间,然后通过调用WithDeadline函数来创建timerCtx。

Deadline方法

timerCtx的Deadline方法会将timerCtx中deadline字段的值进行返回,其对应的源码如下:

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

Done、Err和Value方法

  • timerCtx本身没有实现Done、Err和Value方法,但timerCtx通过嵌套cancelCtx类型的匿名结构体指针的方式继承了cancelCtx,因此在使用timerCtx实例调用Done、Err和Value方法时,会对应调用到cancelCtx实现的Done、Err和Value方法。

cancel方法

timerCtx的cancel方法用于取消当前上下文,其对应的源码如下:

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	// 1、调用cancelCtx的cancel方法完成上下文的取消操作
	c.cancelCtx.cancel(false, err, cause)
	// 2、如果removeFromParent参数为true,则将当前context从其父context中移除
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	// 3、停止定时器,并将其设置为nil
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

cancel方法的执行逻辑如下:

  1. 通过timerCtx的cancelCtx字段,调用cancelCtx的cancel方法完成上下文的取消操作,包括设置err和cause字段、关闭Done通道以及遍历children集合依次取消各个子context。
  2. 如果removeFromParent参数为true,则将当前context从其父context中移除。
  3. 停止timerCtx中定时器,并将timer字段设置为nil,表示定时器已被取消。

说明一下:

  • timerCtx在执行取消操作时,借助cancelCtx的cancel方法完成了大部分的取消操作,但没有复用其中removeChild函数的调用,因为在cancelCtx的cancel方法中调用removeChild函数是将当前的cancelCtx从其父context中移除,相当于将timerCtx中的cancelCtx字段从其父context中移除,而我们要做的是将当前的timerCtx从其父context中移除,因此该动作只能在timerCtx的cancel方法中完成。
  • 由于timerCtx实现了cancel方法和Done方法,其中Done方法是继承的cancelCtx的Done方法,意味着timerCtx也实现了canceler接口,因此cancelCtx的children字段中不仅可以保存cancelCtx,还可以保存timerCtx。

valueCtx

valueCtx

valueCtx是context包中的一个结构体类型,用于在上下文中存储键值对,它实现了Context接口,其定义如下:

type valueCtx struct {
	Context
	key, val any
}

valueCtx结构体各字段说明:

  • Context:嵌套的Context接口类型的匿名结构体,表示当前valueCtx所继承的父上下文。
  • key:表示上下文中存储的键。
  • value:表示上下文中存储的键对应的值。

说明一下:

  • valueCtx中的key和value字段都是any类型的,any是interface{}的别名,表示任意类型。

创建valueCtx

通过context包中的WithValue函数可以创建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")
	}
	// 1、如果key对应的类型不支持比较操作,则抛出panic异常
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	// 2、创建valueCtx实例,并对其各个字段进行初始化
	return &valueCtx{parent, key, val}
}

创建valueCtx的流程如下:

  1. 如果key对应的类型不支持比较操作,则抛出panic异常。
  2. 创建valueCtx实例,并用给定的父context、key和value对其各个字段进行初始化

说明一下:

  • WithValue函数中通过反射机制,保证存入valueCtx中的key必须支持比较操作,因为后续在valueCtx中查找指定key对应的value时,需要对key进行比较操作。

Deadline、Done和Err方法

  • valueCtx主要用于在上下文中存储键值对,而不涉及上下文的取消以及截止时间的管理,因此valueCtx本身并没有实现Deadline、Done和Err方法。
  • 在调用valueCtx的Deadline、Done和Err方法时,会调用到其父context对应的方法,如果其父context仍然没有实现对应的方法,那么会继续沿着context树往上查看各个祖先context是否实现了对应的方法,如果其祖先context都没有实现对应的方法,那么最终会调用到根context,即emptyCtx对应的方法。

Value方法

valueCtx的Value方法会根据指定的key获取上下文中关联的value,其对应的源码如下:

func (c *valueCtx) Value(key any) any {
	// 1、如果当前context中的key与给定的key匹配,则返回context中的value
	if c.key == key {
		return c.val
	}
	// 2、调用value函数在依次在其祖父context中查找给定key对应的value值
	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 // 向父context迭代
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context // 向父context迭代
		case *timerCtx:
			if key == &cancelCtxKey {
				return ctx.cancelCtx
			}
			c = ctx.Context // 向父context迭代
		case *emptyCtx:
			return nil
		default: // 用户自定义实现的Context
			return c.Value(key)
		}
	}
}

Value方法的执行逻辑如下:

  1. 如果当前valueCtx中的key字段与给定的key匹配,则直接返回valueCtx中的value字段。
  2. 如果给定的key与当前valueCtx中的key字段不匹配,则进一步调用value函数依次在其祖父context中查找给定key对应的value值。

value函数会不断向当前context的父context迭代,并对每个context进行类型断言,根据context的类型执行相应的判断逻辑。具体的判断逻辑如下:

  • 如果context的类型是valueCtx,则判断所给key与valueCtx的key字段是否匹配,如果匹配则返回valueCtx对应的value字段,否则继续向父context迭代。
  • 如果context的类型是cancelCtx或timerCtx,则判断所给key是否满足cancelCtxKey约定,如果满足则返回对应的cancelCtx,否则继续向父context迭代。
  • 如果context的类型是emptyCtx,则说明上下文中不存在给定的key值,返回nil。
  • 如果context的类型是用户自定义实现的Context,则调用其对应的Value方法查找给定key对应的value值。

说明一下:

  • 由于每个valueCtx中只存储了一个键值对,因此每向context中添加一个键值对就需要在当前context的基础上创建一个valueCtx,并且从context中查找指定key对应的value值时,其时间复杂度与当前context的层次有关,context的祖父context越多,那么查找的时间复杂度就越高。
  • 因此不要随意将数据存放在context中,而应该只向context中存放少量与context链路或协程并发调用链路相关的数据,以保证context树不会过于庞大,比如在进行网络编程时,通常会将requestId存放在context中。
  • 每向context中添加一个键值对时都会创建一个新的valueCtx,并没有对存储的键值对进行任何的去重操作,因此当从context中查找指定key对应的value时,得到的结果可能是不同的,这却决于当前的context位于context树中的哪一个位置。
  • 如果在协程的启动函数中没有调用WithXxx函数,在已有context的基础上创建出新的context,那么该协程持有的context与其父协程持有的context是同一个,因此一个context可能会被多个协程访问。而一旦协程要向context中添加键值对,那么就需要创建出一个新的context,因此每个valueCtx中只存储一个键值对,在无形中解决了向context中添加键值对的并发写问题。

context使用案例

协程取消

协程取消

使用context实现协程取消的步骤如下:

  1. 在需要被取消的协程A对应的启动函数中,通过调用context包中的WithCancel函数,在已有context的基础上创建一个cancelCtx。
  2. 协程A在运行过程中如果需要创建新协程,则通过协程的启动函数将当前带有取消能力的context传递给各个新协程。
  3. 协程A在运行过程中创建的各个新协程,通过select的方式监听context的Done通道,当context被取消时结束协程的运行。
  4. 当需要取消协程A时,通过调用创建cancelCtx时获得的取消函数,执行context的取消操作,这时当前context及其所有子孙context都会被取消,而所有监听这些context的Done通道的协程,在得知其持有的context被取消后就会退出。

示例代码如下:

package main

import (
	"context"
	"fmt"
	"time"
)

func test(ctx context.Context, name string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	for {
		// 监听context的取消信号
		select {
		case <-ctx.Done(): // context已被取消
			return
		default:
			fmt.Printf("%s goroutine running...\n", name)
			time.Sleep(time.Second)
		}
	}
}

func process(ctx context.Context, name string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	ctx, cancel := context.WithCancel(ctx) // 创建一个派生的cancelCtx

	go test(ctx, "B")
	go test(ctx, "C")

	for i := 0; i < 3; i++ {
		fmt.Printf("%s goroutine running...%d\n", name, i+1)
		time.Sleep(time.Second)
	}
	cancel() // 调用取消函数
}

func main() {
	ctx := context.Background() // 创建一个空的context

	go process(ctx, "A")

	for {
		fmt.Println("main goroutine running...")
		time.Sleep(time.Second)
	}
}

在上述代码中,协程A在执行完三次打印后会取消其持有的context并退出,同时协程A在运行过程中创建的协程B和协程C,在收到context的取消信号后也会相继退出。程序运行结果如下:

在这里插入图片描述

超时控制

超时控制

使用context实现协程超时控制的步骤如下:

  1. 在需要进行超时控制的协程A对应的启动函数中,通过调用context包中的WithDeadline函数,在已有context的基础上创建一个timerCtx。
  2. 协程A在运行过程中如果需要创建新协程,则通过协程的启动函数将当前带有截止时间管理能力的context传递给各个新协程。
  3. 协程A及其在运行过程中创建的各个新协程,通过select的方式监听context的Done通道,当context被取消时结束协程的运行。
  4. 当context的截止时间到达时,timerCtx的定时器timer会自动执行context的取消操作,这时当前timerCtx及其所有子孙context都会被取消,而所有监听这些context的Done通道的协程,在得知其持有的context被取消后就会退出。

示例代码如下:

package main

import (
	"context"
	"fmt"
	"time"
)

func test(ctx context.Context, name string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	for {
		// 监听context的取消信号
		select {
		case <-ctx.Done(): // context已被取消
			return
		default:
			fmt.Printf("%s goroutine running...\n", name)
			time.Sleep(time.Second)
		}
	}
}

func process(ctx context.Context, name string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	deadline := time.Now().Add(3 * time.Second)
	ctx, _ = context.WithDeadline(ctx, deadline) // 创建一个派生的timerCtx

	go test(ctx, "B")
	go test(ctx, "C")

	for {
		// 监听context的取消信号
		select {
		case <-ctx.Done(): // context已被取消
			return
		default:
			fmt.Printf("%s goroutine running...\n", name)
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ctx := context.Background() // 创建一个空的context

	go process(ctx, "A")

	for {
		fmt.Println("main goroutine running...")
		time.Sleep(time.Second)
	}
}

在上述代码中,协程A创建的timerCtx会在三秒后自动被取消,此时协程A及其在运行过程中创建的协程B和协程C,在收到context的取消信号后都会相继退出。程序运行结果如下:

在这里插入图片描述

数据共享

数据共享

使用context实现协程数据共享的步骤如下:

  1. 在协程A的启动函数中,通过调用context包中的WithValue函数,在已有context的基础上创建一个valueCtx,将需要共享的数据以键值对的方式添加到valueCtx中。
  2. 协程A在运行过程中如果需要创建新协程,则通过协程的启动函数将存有数据的context传递给各个新协程。
  3. 协程A在运行过程中创建的各个新协程,通过调用context的Value方法,即可从context中获取指定key对应的value。

示例代码如下:

package main

import (
	"context"
	"fmt"
	"time"
)

func getId(ctx context.Context) (processId string, testId string) {
	processId, ok := ctx.Value("process_id").(string)
	if !ok {
		processId = "no process_id"
	}
	testId, ok = ctx.Value("test_id").(string)
	if !ok {
		testId = "no test_id"
	}
	return
}

func test(ctx context.Context, name string, id string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	ctx = context.WithValue(ctx, "test_id", id) // 创建一个派生的valueCtx

	processId, testId := getId(ctx)
	for {
		fmt.Printf("%s goroutine...process_id = %s, test_id = %s\n",
			name, processId, testId)
		time.Sleep(time.Second)
	}
}

func process(ctx context.Context, name string, id string) {
	defer fmt.Printf("%s goroutine exit...\n", name)

	ctx = context.WithValue(ctx, "process_id", id) // 创建一个派生的valueCtx

	go test(ctx, "B", "001")
	go test(ctx, "C", "002")

	processId, testId := getId(ctx)
	for {
		fmt.Printf("%s goroutine...process_id = %s, test_id = %s\n",
			name, processId, testId)
		time.Sleep(time.Second)
	}
}

func main() {
	ctx := context.Background() // 创建一个空的context

	go process(ctx, "A", "8421")

	processId, testId := getId(ctx)
	for {
		fmt.Printf("main goroutine...process_id = %s, test_id = %s\n",
			processId, testId)
		time.Sleep(time.Second)
	}
}

在上述代码中,协程A在运行过程中向context中添加了process_id,其创建的协程B和协程C又向context中添加了test_id。context树大致如下:

在这里插入图片描述

主协程持有的context是图中的emptyCtx,协程A持有的context是图中的valueCtx1,协程B和协程C持有的context分别是图中的valueCtx2和valueCtx3。主协程、协程A、协程B和协程各自持有的是context树中不同的context,因此它们从context中查找指定key对应的value时会得到不同的结果。程序运行结果如下:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/696794.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue-router全部搞定(附源码)

源码下载链接&#xff08;先转存&#xff0c;后下载&#xff09;&#xff1a;https://pan.quark.cn/s/b0c6edd68c21 怎么用vue-cli搭建项目 我们固然可以用传统htmljs的方式来搭建vue项目&#xff0c;但是如果组件很多&#xff0c;就需要通过Vue.component的方式一个个去引入…

Python的else子句7个妙用,原来还能这样用,整挺好!

## 1、条件语句else基础 &#x1f504; 1.1 简单else的常规操作 在Python中&#xff0c;else子句通常跟在if或一系列if-elif之后&#xff0c;提供一个“否则”的情况处理路径。如果前面的所有条件都不满足 &#xff0c;程序就会执行这里的代码块。例如 &#xff0c;检查一个数…

【STM32】GPIO输出(江科大)

一、GPIO简介 1.GPIO&#xff1a;通用输入输出口 2.可配置为8种输入输出模式 3.引脚电平&#xff1a;0-3.3V&#xff08;输出最大3.3V&#xff09;&#xff0c;部分引脚可容忍5V&#xff08;输入&#xff0c;有FT&#xff09; 4.输出模式下&#xff0c;可控制端口输出高低电平…

详解FedProx:FedAvg的改进版 Federated optimization in heterogeneous networks

FedProx&#xff1a;2020 FedAvg的改进 论文&#xff1a;《Federated Optimization in Heterogeneous Networks》 引用量&#xff1a;4445 源码地址&#xff1a; 官方实现&#xff08;tensorflow&#xff09;https://github.com/litian96/FedProx 几个pytorch实现&#xff1a;…

十二、【源码】配置注解执行SQL

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/12-annotation 配置注解执行SQL 简化一下流程&#xff0c;主要可以分为下面几步&#xff1a; 1.解析配置&#xff0c;写入配置项 2.执行…

问题排查: Goalng Defer 带来的性能损耗

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言问题背景结论 引言 性能优化之路道阻且长&#xff0c;因为脱敏规定&#xff0c;…

Nginx 精解:正则表达式、location 匹配与 rewrite 重写

一、常见的 Nginx 正则表达式 在 Nginx 配置中&#xff0c;正则表达式用于匹配和重写 URL 请求。以下是一些常见的 Nginx 正则表达式示例&#xff1a; 当涉及正则表达式时&#xff0c;理解各个特殊字符的含义是非常重要的。以下是每个特殊字符的例子&#xff1a; ^&#xff1…

讯飞星火大模型个人API账号免费使用申请教程

文章目录 1.登录讯飞星火大模型官网 https://www.xfyun.cn/ 2.下滑找到Spark Lite&#xff0c;点击立即调用 3.星火大模型需要和具体的应用绑定&#xff0c;我们需要先创建一个新应用 https://console.xfyun.cn/app/myapp&#xff0c;应用名称可以按照自己的意愿起。 4.填写应用…

打造智慧工厂核心:ARMxy工业PC与Linux系统

智能制造正以前所未有的速度重塑全球工业格局&#xff0c;而位于这场革命核心的&#xff0c;正是那些能够精准响应复杂生产需求、高效驱动自动化流程的先进设备。钡铼技术ARMxy工业计算机&#xff0c;以其独特的设计哲学与卓越的技术性能&#xff0c;正成为众多现代化生产线背后…

ViT:2 理解CLIP

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;则提…

vuInhub靶场实战系列--Kioptrix Level #3

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

快速测试 Mybatis 复杂SQL,无需启动 Spring

快速测试mybatis的sql 当我们写完sql后&#xff0c;我们需要测试下sql是否符合预期&#xff0c;在填入各种参数后能否正常工作&#xff0c;尤其是对于复杂的sql。 一般我们测试可能是如下的代码: 由于需要启动spring&#xff0c;当项目较大的时候启动速度很慢&#xff0c;有些…

④-2单细胞学习-cellchat单数据代码补充版(通讯网络)

目录 通讯网络系统分析 ①社会网络分析 1&#xff0c;计算每个细胞群的网络中心性指标 2&#xff0c;识别细胞的信号流模式 ②非负矩阵分解&#xff08;NMF&#xff09;识别细胞的通讯模式 1&#xff0c;信号输出细胞的模式识别 2&#xff0c;信号输入细胞的模式识别 信…

RocketMq源码解析六:消息存储

一、消息存储核心类 rocketmq消息存储的功能主要在store这个模块下。 核心类就是DefaultMessageStore。我们看下其属性 // 配置文件 private final MessageStoreConfig messageStoreConfig; // CommitLog 文件存储实现类 private final CommitLog commitLog; …

【研发日记】Matlab/Simulink软件优化(三)——利用NaNFlag为数据处理算法降阶

文章目录 前言 背景介绍 初始算法 优化算法 分析和应用 总结 前言 见《【研发日记】Matlab/Simulink软件优化(一)——动态内存负荷压缩》 见《【研发日记】Matlab/Simulink软件优化(二)——通信负载柔性均衡算法》 背景介绍 在一个嵌入式软件开发项目中&#xff0c;需要开…

FedAvg论文

论文&#xff1a;Communication-Efficient Learning of Deep Networks from Decentralized Data 原code Reproducing 通过阅读帖子进行的了解。 联邦平均算法就是最典型的平均算法之一。将每个客户端上的本地随机梯度下降和执行模型的平均服务器结合在一起。 联邦优化问题 数…

开发小Tips:切换淘宝,腾讯,官方,yarn,cnpm镜像源,nrm包管理工具的具体使用方式(方便切换镜像源)

由于开发中经常要下载一些软件或者依赖&#xff0c;且大多数的官方源的服务器都在国外&#xff0c;网速比较慢&#xff0c;国内为了方便&#xff0c;国内一些大厂就建立一些镜像&#xff0c;加快下载速度。 1.各大镜像源的切换&#xff1a; 切换淘宝镜像源&#xff1a; npm …

Bio-Info每日一题:Rosalind-06-Counting Point Mutations

&#x1f389; 进入生物信息学的世界&#xff0c;与Rosalind一起探索吧&#xff01;&#x1f9ec; Rosalind是一个在线平台&#xff0c;专为学习和实践生物信息学而设计。该平台提供了一系列循序渐进的编程挑战&#xff0c;帮助用户从基础到高级掌握生物信息学知识。无论你是初…

数据结构笔记 线性表的查找 顺序,折半,分块查找

顺序查找&#xff1a;从头找到尾&#xff0c;或者从尾找到头 顺序查找的性能&#xff1a; 其中&#xff0c;辅助空间的O&#xff08;1&#xff09;用于存放哨兵的 折半查找&#xff1a;向下取整&#xff1a;指当计算的结果不为整数时取小于计算结果的整数。 折半查找的性能&am…

类和对象的学习总结(一)

面向对象和面向过程编程初步认识 C语言是面向过程的&#xff0c;关注过程&#xff08;分析求解问题的步骤&#xff09; 例如&#xff1a;外卖&#xff0c;关注点菜&#xff0c;接单&#xff0c;送单等 C是面向对象的&#xff0c;关注对象&#xff0c;把一件事拆分成不同的对象&…