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的流程如下:
- 创建一个cancelCtx实例,并用给定的父context对cancelCtx的Context字段进行初始化。
- 调用propagateCancel函数将创建的cancelCtx与其父context关联起来,保证父context被取消时,子context也会被取消。
- 返回创建的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函数的执行逻辑如下:
- 如果父context的Done方法返回的channel为nil,表明父context永远不会被取消,不必实现取消信号的传播,函数直接返回。
- 尝试从父context的Done通道中读取取消信号,如果读取成功则说明父context已经被取消,这时直接将子context也取消即可。
- 如果从父context中获取cancelCtx成功,并且此时父context没有被取消,则将子context添加到父context的children集合中。
- 如果从父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方法的执行逻辑如下:
- 如果cancelCtx的done字段不为nil,表明其对应的channel已经创建过了,直接将其返回即可。
- 如果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方法的执行逻辑如下:
- 如果指定的key为cancelCtxKey的地址,则返回cancelCtx本身。
- 如果指定的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函数的执行逻辑如下:
- 如果context的Done通道为nil或closedChan,表明该context不支持取消操作或已经完成了取消,获取cancelCtx失败,直接返回。
- 调用context的Value方法并传入cancelCtxKey的地址,如果获取cancelCtx失败,则直接返回。
- 如果获取从context中获取cancelCtx成功,但cancelCtx的Done通道与context的Done通道不一致,表明cancelCtx已经发生了变化,此时获取cancelCtx失败返回。
- 如果前面的所有检查都通过,则将获取到的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方法的执行逻辑如下:
- 如果cancelCtx的err字段不为nil,表明上下文已经被取消,无需重复取消,直接返回。
- 如果cancelCtx未被取消,则设置其err和cause字段,表明上下文已经被取消,同时关闭其Done通道,使得监听Done通道的相关协程能够得知该context已经被取消。
- 遍历cancelCtx的children集合,依次取消各个子context,并将children集合设置为nil,实现取消信号的传播。
- 如果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关联起来,可以实现协程的取消。具体操作如下:
- 仅在主协程中通过Background函数创建一个emptyCtx,保证全局只有一个context树,并在每次启动新协程的时候,通过协程的启动函数将当前的context传递给各个新协程。
- 如果一个协程在某些情况下需要被取消,则在该协程中通过WithCancel函数在已有context的基础上创建一个带有取消功能的cancelCtx,并将其传递给该协程创建的所有新协程,使得该协程创建的所有新协程所持有的context都带有取消能力。
- 当用户调用创建cancelCtx时获得的闭包函数,执行context的取消操作时,这个取消事件会从当前context处开始,依次向其子孙context传播,最终导致其所有的子孙context都被取消,即当前协程所创建的各个新协程所持有的context都会被取消。
- 各个协程在执行过程中,通过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的流程如下:
- 如果给定的父context存在截止时间,并且其截止时间比子context的截止时间早,那么子context没有必要对截止时间进行管理,因为父context会在子context的截止时间到达之前被取消,这时子context也会被连带取消,因此创建一个cancelCtx返回即可。
- 如果给定的父context不存在截止时间,或其截止时间比子context的截止时间晚,这时子context需需要创建为timerCtx。在创建timerCtx时,分别根据所给的父context和截止时间,对timerCtx的Context字段和deadline字段进行初始化。
- 调用propagateCancel函数将创建的timerCtx与parent关联起来,保证父context被取消时,子context也会被取消。
- 如果timerCtx的截止时间在当前时间之前,则调用timerCtx的cancel方法对其进行取消后返回。
- 如果timerCtx的截止时间还未到达,则创建timerCtx的定时器timer字段,使其在截止时间时间到达时执行timerCtx的取消操作。
- 返回创建的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
上一篇:
QPS,平均时延和并发数
下一篇:
MySQL数据库(二)和java复习
推荐阅读
-
详解YUV系列(三)-------YUV420
-
解密Android Bitmap转I420的难题,附图文详解YUV420数据格式
-
YUV420数据格式详解,以图文形式揭示 - 提示:加强阅读理解,配合图片观看更加易懂
-
详解YUV444、YUV422、YUV420的可视化解释
-
【Codecs】解析YUV420/YUV422数据格式:图文详解 (全面而详细)
-
深入解析yuv420格式——图文详解
-
【音视频系列1】视频格式YUV444、YUV422,、YUV420详解
-
深入解析YUV420、YUV420(YUY2)、YUV422(YVYU):以图解方式详解
-
解决Android yuv420详解的具体操作步骤
-
详解 YUV 格式(I420/YUV420/NV12/NV12/YUV422)-参考资料