Context
介绍
如它的字面意思,Context
可用来传输数据。但它还有一个别的功能,就是用来取消一个协程的执行。但总之,Context
根源是基于上下文的这样一个东西。
传输数据功能
Context
本身是个 interface
类型的结构体,它有四个方法:Deadline()
、Done()
、Error()
、Value()
Deadline()
用于获取Context
的截止时间Done()
用于返回一个只读的Channel
,用于通知当前Context
是否已经被取消。Err()
用于用去Context
取消的原因Value()
用于获取Context
中存储的键值对数据
//Go语言源码内
type emptyCtx int
//当这个上下文没有过期时间的时候,可以通过后面这个bool值标明。
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return //空白上下文,直接返回默认时间和false,false会导致这个上下文永不结束
}
//返回一个只读的channel,这个channel可以作为一个信号发射器来反映context是否停止。
//我们知道go语言中的struct本身是一个空类型,所以channel不做数据存储用途。
//上面这一条也证实是用来作为一个通道信息的信号发射器。
func (*emptyCtx) Done() <-chan struct{} {
return nil //因为nil,会导致任何想要读取emptyCtx内容都会阻塞(因为Channel的特性)
}
//context生命周期终止返回一个err。
//这个方法利于通过同步调用方式直接感知context现在是什么情况。
func (*emptyCtx) Err() error {
return nil //空白上下文的err直接是空,也就是没有错误
}
//这个方法是context数据存储能力的支撑,通过key获取value。
func (*emptyCtx) Value(key any) any {
return nil //空白上下文的value返回空,也就是没有任何的数据存储在里面
}
从上面我们会发现,官方给了我们一个空的上下文
emptyCtx
,这个空的上下文就和一张空白白纸一样,上面啥都没,没有任何的属性。实际我们创建的每一个
Context
,它们的祖辈Context
一定是这一个emptyCtx
。这个
emptyCtx
虽然实际上啥都没有,但也实现了一个最基本的Context
架构。我们之后的任何
Context
操作,都是在这一个最基本的架构上进行叠加实现的。也就是说,一个父
Context
可以作为一个加了新功能的子Context
的基底,它们之间是有基底联系的。一个父Context
可有有多个加有不同功能的子Context
。所以Context
一定会形成一个类似树状的结构,那个最开始的父Context
(也就是emptyCtx
)就是这棵树的根节点。当有一个
Context
生命周期终止了,那么它作为父节点的下面的子Context
就会被父Context
单向传递终止事件,使它的子Context
们也跟着终止,子Context
的孙Context
们也会跟着终止,以此类推。注意:这个过程只能是单向的从父→子。
因为上面这种传递终止的特性,使
Context
得以服务于父子协程的并发控制,让这些协程知道什么时候该结束,以及触发结束的条件是什么。但这些都是
Context
取消功能的事,不属于传递数据功能的事,后话再说(
context.backgroud
和 context.todo
都是用于创建上下文,都是属于 emptyCtx
。
//Go语言源码内
type emptyCtx int
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
这里我们写一段示例代码来看看 Context
具体是怎样的:
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "name", "RoLingG")
getContext(ctx)
}
func getContext(ctx context.Context) {
fmt.Println(ctx.Value("name"))
}
因为 WithValue()
传的是 any
类型,所以甚至能在 value
处传递一个结构体。
type userName struct {
Name string
}
func main() {
ctx := context.Background()
//ctx = context.WithValue(ctx, "name", "RoLingG")
ctx = context.WithValue(ctx, "name", userName{Name: "RoLingG"})
getContext(ctx)
}
func getContext(ctx context.Context) {
fmt.Println(ctx.Value("name").(userName).Name) //使用断言解析结构体的内容
}
一个协程,当你不知道什么时候终止它,那就不要创建它。
——不知道谁说的,但算是个准则。
这句话背后的含义就是:不要滥用并发。
要使用并发的时候,就要有并发控制这一概念,要去有目的的使用并发。无目的的滥用并发,可能会引起各种性能问题,严重的可能引起协程泄露。
所以Context
有三种取消方式去解决并发控制问题。
取消功能
Context
一共有三种取消方式:①取消协程、②截止时间取消 ③超时时间取消
通过Context
可以有效的帮我们控制协程的资源,当一个协程不符合我们的需求的时候便可以使用Context
去取消掉它从而达到回收资源的效果。
取消协程
withCancel
例如一个app内执行的功能的取消按钮执行的功能,Context的取消协程就类似这个例子,可以将正在执行的协程取消掉。
var wait = sync.WaitGroup{} func getIP(ctx context.Context) (ip string, err error) { go func() { select { case <-ctx.Done(): fmt.Println("协程被取消", ctx.Err()) //模拟实际网络超时,超过取消检测时间,导致协程被取消,释放资源 err = ctx.Err() wait.Done() return } }() time.Sleep(4 * time.Second) //之所以上面协程getIP没有获取到ip是因为这里时间写死了,没能模拟到实际网络情况,如果实际流畅的话这里的时间很短,是可以获取到IP的 ip = "127.0.0.1" wait.Done() return } func main() { t1 := time.Now() ctx, cancel := context.WithCancel(context.Background()) //创建取消协程的上下文 wait.Add(1) go func() { ip, err := getIP(ctx) //让取消协程的上下文作为参数进行控制 //if err != nil { // return //} fmt.Println(ip, err) }() go func() { time.Sleep(2 * time.Second) //取消协程 cancel() //使用取消协程的方法进行取消对应的协程,因为ctx被上面那个协程使用,所以这里的cancel()是取消上面的协程 }() wait.Wait() fmt.Println("执行成功", time.Since(t1)) }
截止时间取消
WithDeadline
在超时的情况下取消请求。会将相对时间转换为绝对时间。也就是传入一个结束时间点。
func main() { var wg = sync.WaitGroup{} t1 := time.Now() ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) //创建取消协程的上下文 wg.Add(1) go func() { ip, err := getIP1(ctx, &wg) //触发截止时间取消上下文,截止后超过2s立刻取消该协程 fmt.Println(ip, err) }() wg.Wait() fmt.Println("执行成功", time.Since(t1)) } func getIP1(ctx context.Context, wg *sync.WaitGroup) (ip string, err error) { go func() { select { case <-ctx.Done(): fmt.Println("协程超时", ctx.Err()) //模拟实际网络超时,超过检测时间,导致协程被取消,释放资源 err = ctx.Err() wg.Done() return } }() time.Sleep(4 * time.Second) //4秒超2秒检测,超时取消协程 ip = "127.0.0.1" wg.Done() return }
超时时间取消
WithTimeout
用法与截止时间基本一致,本身就是基于截止时间去实现的,但协议不一样,还要传入一个距离当前时刻的相对的时间。也就是说传入一个持续时间。
func main() { var wg = sync.WaitGroup{} t1 := time.Now() ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) wg.Add(1) go func() { ip, err := getIP2(ctx, &wg) //触发截止时间取消上下文,协程运行超过2s则立刻取消该协程 fmt.Println(ip, err) }() wg.Wait() fmt.Println("执行成功", time.Since(t1)) } func getIP2(ctx context.Context, wg *sync.WaitGroup) (ip string, err error) { go func() { select { case <-ctx.Done(): fmt.Println("协程超时", ctx.Err()) //模拟实际网络超时,超过检测时间,导致协程被取消,释放资源 err = ctx.Err() wg.Done() return } }() time.Sleep(4 * time.Second) //4秒超2秒检测,超时取消协程 ip = "127.0.0.1" wg.Done() return }
后日谈:
实际上这里应该还涉及的了
cancelCtx
、timerCtx
和valueCtx
这些底层的东西,具体的话还是去找找别人的资料吧,我也才刚学。用于取消协程的功能时:
cancelCtx
一定要作为一个子Context
而存在,它必须要有一个父Context
作为支撑。
timerCtx
其实很大程度上是基于cancelCtx
的基础上完成操作的,它的源码层面就继承了cancelCtx
,并且重写了cancel
方法。所以timerCtx
一定程度上依附着cancelCtx
。
细看cancelCtx
cancelCtx的源码:
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
}
- 源码中我们第一个看
Context
,它很好的印证了“cancelCtx
一定要作为一个子Context
而存在,它必须要有一个父Context
作为支撑”这句话。 - 第二个看
mu
,这是一个互斥锁,当在并发环境下,并发操作一个资源时,可以通过这个互斥锁去保证操作的一致性。 - 第三个看
done
,这是一个只读的channel
,这个和上面的emptyCtx
的done
几乎是一样的,用于感知当前这个Context
生命周期的变化。 - 第四个看
children
,这是一个指向当前Context
的子Context
的集合,里面的map[canceler]
的canceler
是一个interface
类型,也就是说是一个数据接口,它的任何子Context
新添加的功能都与当前这个父Context
无关。当父Context
生命周期结束关闭时,子Context
添加的新功能实现应该由它自己本身去解决。(大概是这样吧,第一次看源码,还不太懂) - 第五个看
err
,当前Context
生命周期终止时,err
被用来将错误对象会被传递给使用此上下文的代码,以便其知道上下文已经被取消。它提供了一种通用的方式来传递取消操作的信号,并且通常包含了关于取消操作本身的信息。 在更新版本超过
Go 1.19
之后,Context
中的cancleCtx
添加了一个新的对象cause
,这是为了帮助开发人员更好地理解和调试问题。第六个
cause
字段提供了更具体的原因或根源信息,用于解释为什么上下文被取消。它可能包含关于取消操作背后更详细的上下文或场景。在某些情况下,cause
字段可能包含原始的错误信息或异常的堆栈跟踪,以提供给开发人员更深入的洞察力。这里就体现出了Go语言的特性,Go 语言中之所以没有像其他语言一样有继承的概念,就是为了将父与子区分开,两个该干嘛干嘛,谁实现的东西谁去管理,而不是子新实现的东西交给父来管理,划分清楚边界,职责内聚。
cancelCtx的Deadline()
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
的源码,我们会发现cancelCtx
并没有Deadline()
这个方法。所以如果要调用这个方法,cancelCtx
会直接叫父Context
去实现这个方法。如果没有的话,就返回一个初始的时间
和一个false
的bool值。
cancelCtx的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{})
}
cancelCtx
的done()
方法,我们通过源码可以看出除了与普通的Context
一致,都是读取使用cancelCtx
的这个channel
以外,还有一个互斥锁,这是为了保证channel
的懒加载(初始化channel
存储到aotmic.Value
当中,并返回)和在并发环境下的一致性操作。如果去除掉并发环境下,那这个代码就是简单的获取channel
,检查这个channel
是否存在,存在则返回。
cancelCtx的err()
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
cancelCtx
的err()
这一个方法,也会存在并发读写的情况,所以在普通Context
的err()
方法读取cancelCtx.err
的基础上也加了一个互斥锁。
cancelCtx的cause的相关方法
至于cause
,我们看cancelCtx
的源码:
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
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, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
会发现它实际上用在cancel()
这个方法中,cause
的存在是为了提供比 err
更详细的信息。err
通常只是一个通用的错误对象,表示上下文已被取消,而 cause
则提供了导致取消操作发生的具体原因或背景。这对于调试和错误处理非常有用,因为它可以帮助开发人员更深入地了解为什么上下文被取消。
cancelCtx的value()
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
根据源码来看,倘若key
为特定值&cancelCtxKey
,那么就返回cancelCtx
自身的指针。如果不是,则遵循valueCtx
的思路取值返回。
注意:这个&cancelCtxKey
是不可导出的类型,所以不可能是外部的用户写的,一定是在本程序代码内声明出来调用的。
cancelCtx的withCancel()
// WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc.
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
// Calling cancel with nil sets the cause to Canceled.
//
// Example use:
//
// ctx, cancel := context.WithCancelCause(parent)
// cancel(myError)
// ctx.Err() // returns context.Canceled
// context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
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")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c
}
从上述源码可以看出withCancel()
方法中的cancelCtx
的父Context
不能为空。不为空之后将父Context
注入一个新的Context
中,这样就获得了一个新的子Context
。propagateCancel(parent, c)
传播取消信号保证了如果父Context
被取消了,那么它的子Context
也要跟着被取消。
然后新的子Context
返回给WithCancelCause()
,往下走就到return c, func(cause error) { c.cancel(true, Canceled, cause) }
,这代表WithCancelCause()
最终连带返回了一个以终止该cancelCtx
的闭包函数。只要调用这个闭包函数,withCancel
保证创建出来的新子Context
就会被取消,同时以它作为父Context
的所有子Context
也会因为取消信号一并被取消。
cancelCtx的newCancelCtx()
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
这个方法被调用就很根据传入的父Context
去创建一个新的子cancelCtx
,它的父Context
即为传入的这个parent Context
。
cancelCtx的propagateCancel()
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
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 {
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
根据之前的描述我们可以知道propagateCancel()
这个方法就是为了传播取消信号。我们根据源码一行行看下来会发现它其实有对父Context
的done()
方法传出来的值做校验,还有挺多的判断,这些判断能为它很好的选出情况并做出对应的解决方法。
当parent.Done()
为nil
的时候,那么代表这个父Context
永远都不会被取消终止,那也就意味着没有必要去进行取消传播。
当父Context
已经开始被取消时,那么子Context
也必须跟着取消,子Context
直接调用cancel
进行取消,err用父Context
的,来源也于父Context
。
接着往下,当父Context
也是cancelCtx
,那么只需要保证子Context
能添加进父cancelCtx
的children
集合中去就行。这样能基于cancelCtx
自身的取消机制,连着子cancelCtx
集合一并取消。
假如父Context
不是cancelCtx
,那么它会在propagateCancel()
内开启一个异步的协程,使用一个select
的多路复用框架,时刻检测父与子的channel
,如果父Context
生命周期终止,就能从父Context
中获取到取消信号,那么同时让子Context
使用cancel()
就能一起取消掉了;如果子Context
比父Context
更早被取消,那么就啥都不用做,因为取消信号不能逆向而上,只能单向向下。
但这如何判断父Context
是cancelCtx
呢,答案就在下面。
cancelCtx的parentCancelCtx()
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
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
}
从上往下看源码,一开始还是一如既往地判断是有父Context
为空,因为cancelCtx
必须要父Context
,所以为空就不用判断了,直接返回。
我们接着往下看,我们会发现这里用到了前面value()
中的&cancelCtxKey
,这里其实我们就可以明白了源码是如何来判断父Context
是否是cancelCtx
了,就是通过这个特殊的指针&cancelCtxKey
。对于cancelCtx
而言,在调用value()
这个方法,时,会返回它自身,也就是说cancelCtx
的key
必为&cancelCtxKey
。
那么,如果通过p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
这条代码检测得出ok
为true
,那么通过if !ok
条件进行判断不成立的话,那么父Context
就一定是cancelCtx
。
cancelCtx的cancel()
我们该如何去取消当前的Context
,并保证上游的Context
也能够接收到,这一点官方的源码也写的很详细:
// WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc.
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
// Calling cancel with nil sets the cause to Canceled.
//
// Example use:
//
// ctx, cancel := context.WithCancelCause(parent)
// cancel(myError)
// ctx.Err() // returns context.Canceled
// context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
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")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c
}
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
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, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
这里我们要连着withCancel()
这个方法去看,在当前Context
要取消时,它需要给它自身的err
存入被取消的err
,同时让cause
存入err
用于帮助开发人员查询信息。同时还要保证当前的chan
能被上游的Context
调用done()
方法能够读取到信号,而不是被阻塞。cancelCtx
有义务将它下游的所有子Context
在它被取消时也同时被取消。
细看valueCtx
Context
用于存储功能时:
// 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
}
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)
}
}
}
valueCtx
一定会有一个父Context
,不能当祖节点。
我们可以看看源码,会发现valueCtx
的key/value
不是map
,而是一个key-value
对。设置了多个key-value
对,就代表有多个作为子节点的valueCtx
产生。
如果我们调用Value()
方法,它会去匹配当前这个Context
的key
是否为传入的key
相互匹配。如果匹配上,则直接返回;如果不行,则而外调用value()
方法进一步匹配。我们看value()
方法里就会发现其实valueCtx
与cancelCtx
是有强相关关系的,在switch
的选择里会去判断当前的Context
是否是cancelCtx
。
我们但看valueCtx
的情况,源码缩写大概就是下面这样:
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
default:
return c.Value(key)
}
}
我们会发现如果当前的Context
是valueCtx
那么在Value()
没匹配到的情况下,value()
它会不断地循环往上拿父Context
去匹配。这是一个从子→父
找的过程,直至匹配成功或者失败。
时间复杂度会有问题,一般为O(N),因为是类链路的形式走,也就是线性行走,不是常数级的时间复杂度。另外每一次创建一个新的valueCtx
的代价都比较昂贵,因为做的东西可能少,但是创建的确实一整个利用率不高的valueCtx
。所以valueCtx
不能当做是存放业务数据的存在,而是类似于header
一样的存在,只存放少量作用大的全局mate
数据。
还有,当我们两次为一个相同的key
放入值的时候,它不会有去重的机制,而是生成两个新的不同的子valueCtx
去存储这两个值。我们之后去寻址的时候,寻找这个key
,那么这个key
返回的结果是不确定的,取决于调用context.value
的节点在哪,在的节点有key
对应的value
就会就近取它;但如果没有,则会跑到当前节点的父节点去匹配,以此类推。
例如:valueCtx
要找键值为c
的,我们从键值为f
的Context
开始找,往上找一个父节点就能找到了。但如果我们从键值为e
的节点开始找,就不可能找到,直接一直往上直至全部匹配失败。
但这不是Context
的一个缺点,相反它是故意这么做的。因为Context
是作用于并发的场景中,我们都知道并发读操作是没有问题的,但如果参入了并发写,那就会出问题了。
这样故意分散开key
的对应值就会使其即便在并发的环境中,也不用担心并发写的问题。因为数据都被打散在不同的节点中,不会相互影响。保证了这些key-value
对是不同子协程中独有的数据。
当然也可以通过withValue()
方法进行不同协程之间共享key-value
对。
评论(0)