Golang Context 作者: nbboy 时间: 2021-05-10 分类: 软件架构,设计模式,C,Golang # Context > 使用上下文的一个很好的心理模型是它应该在您的程序中流通,想象一条河或流水。 **分析版本:1.15.2** ### Context context接口只有4个方法,先看下接口定义 ```go type Context interface { //超时时间 Deadline() (deadline time.Time, ok bool) //需要监听的通道 Done() <-chan struct{} //如果没有关闭,则返回nil,否则返回一个错误值 Err() error //指定key的value Value(key interface{}) interface{} } ``` 目前版本中,实现的struct为emptyCtx,cancelCtx,timerCtx,valueCtx,每个ctx对应的应用场景都不一样,先看下最简单的emptyCtx ### emptyCtx ```go var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } ``` TODO和Background用的都是emptyCtx,Background主要被用来作为其他Ctx的根,而TODO主要可以视为一种nil的Ctx去用,因为在官方的设计中,不允许使用nil作为Ctx的值。emptyCtx的实现非常简单,不做具体介绍,都是空的方法体。 先看下比较常用的cancelCtx的使用方法 ### cancelCtx ```go gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done()://取消后从这里返回 fmt.Println("ctx.done") return case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) for n := range gen(ctx) { fmt.Println(n) if n == 5 { //达到目标,取消ctx cancel() break } } time.Sleep(3 * time.Second) ``` cancelCtx主要用来控制goroutine的生命周期,即什么时候结束生命周期,当然这个需要goroutine本身去配合,select Done返回的通道。再看下,cancelCtx的内部结构 ```go type cancelCtx struct { Context mu sync.Mutex // protects following fields 用来保护成员 done chan struct{} // created lazily, closed by first cancel call 就是Done()返回的chan,调用cancel()后就被关闭 children map[canceler]struct{} // set to nil by the first cancel call 子ctx,所有ctx会组成一颗树形结构,而此处指向其孩子节点 err error // set to non-nil by the first cancel call 调用cancel()后,被设置成取消原因 } ``` 这里看下cancelCtx的构建函数 ```go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } //创建cancelCtx,关联传递进来的ctx c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } func propagateCancel(parent Context, child canceler) { //首先检查父ctx是否关闭 done := parent.Done() if done == nil { return // parent is never canceled } select { case <-done: // parent is already canceled // 如果父ctx被取消,则也同步取消子ctx child.cancel(false, parent.Err()) return default: } //如果找到了cancelCtx if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil {//父节点已经被取消 // parent has already been canceled child.cancel(false, p.err) } else {//如果没被取消,则把子节点挂到父节点 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { //如果是自定义的ctx,就会开启一个goroutine去监听父的取消事件,并且取消子ctx atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() //如果关闭,则返回false if done == closedchan || done == nil { return nil, false } //看下是否cancelCtx p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) //没找到则返回false if !ok { return nil, false } p.mu.Lock() ok = p.done == done p.mu.Unlock() //如果不一样也返回flse if !ok { return nil, false } //通过深层查找,找到了cancelCxt,则才返回true return p, true } ``` - 父Ctx如果不需要被取消,则直接返回,Background,TODO就是不需要被取消的类型 - 如果父ctx被取消,则也同步取消子ctx - parentCancelCtx会深层次得去找父cancelCtx,这里分两种情况 1)如果是标准(cancelCtx,timerCtx)则会同步父子Ctx的状态(要么都同步取消,要么建立关系) 2)如果是自定义Ctx,就会开启一个goroutine去监听父的取消事件,并且取消子ctx 这里这么做的原因,就是需要把子节点的状态和父节点要同步,调用withCancel()返回的cancel函数其实是调用cancelCtx.cancel()函数 ```go // 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 if c.done == nil { c.done = closedchan } else { //关闭chan close(c.done) } //子ctx依次进行cancel 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() //从根节点里移除c if removeFromParent { removeChild(c.Context, c) } } ``` 注释中说的很明白,会把打开的chan关闭,然后依次调用子ctx的cancel,所以如果我们忘记调用cancel,其实会有大量的chan没被close掉,然后造成资源的浪费! 此处试下级联取消,代码如下 ```go func main() { ctx1, cancel1 := context.WithCancel(context.Background()) defer cancel1() ctx2, cancel2 := context.WithCancel(ctx1) defer cancel2() ctx3, cancel3 := context.WithCancel(ctx2) defer cancel3() go func() { select { case <-time.After(3 * time.Second): cancel1() } }() <-ctx3.Done() } ``` 创建了三个ctx,然后第一个ctx取消后,其下的所有ctx都会取消。需要注意,代码中其实ctx1被cancel了两次,通过了解实现的代码,知道这么写其实并没有什么问题。画一个图,直观了解下3个ctx组成的结构。取消是沿着继承链,从除了根部外(Background不能被取消)一直到所有节点执行取消操作! ┌───────────────────┐ │ ┌───────────────┐ │ │ │ Background │ │ │ └───────────────┘ │ └─────────┬─────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ │ │ │ Ctx1 │ │ │ │ └───────────────┘ │ │ └─────────┬─────────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ Cancel │ │ Ctx2 │ │ │ │ └───────────────┘ │ │ └─────────┬─────────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼─────────┐ │ │ ┌───────────────┐ │ │ │ │ Ctx3 │ │ │ │ └───────────────┘ │ ▼ └───────────────────┘ ### timerCtx 其实Context最牛的功能我觉得还是timerCtx,先来看一下这个功能一个简单的例子。 ```go func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() result := make(chan int, 3) for i := 0; i < 10; i++ { go func(i int) { for { select { case <-ctx.Done(): fmt.Println("return") return case result <- i: } } }(i) } for { select { case r := <-result: fmt.Println(r) case <-ctx.Done(): return } } } ``` 其实这个ctx就是定义了一个具有超时功能的上下文,一般可以应用在可能会长时间执行的任务上,如果该任务长时间执行,我们可以设置一个ctx,超时时间到来,goroutine就从该任务返回,不会造成任务失控的情况。继续看下timerCtx的结构 ```go // 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,所以也具有取消的能力 cancelCtx timer *time.Timer // Under cancelCtx.mu. //超时时间点 deadline time.Time } ``` WithTimeout其实内部会调用WithDeadline,我们分析下该方法 ```go 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) //时间已经到了,就直接cancel 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() //用定时器去处理延迟cancel if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } ``` 这里有3点要注意: 1. 如果子Ctx超过了父Ctx则,直接使用父Ctx 2. 如果时间已经到期,则直接Cancel 3. 否则就注册一个定时器在未来一个时间执行 这里比较关心的是,他内部其实维护了一个定时器,就是那么简单而已!!!在分析一下对应的cancel方法 ```go 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() } ``` 比cancelCtx多了停止定时器的操作。 ### valueCtx WithValue很容易建立valueCtx,valueCtx结构如下 ```go // 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 interface{} } func WithValue(parent Context, key, val interface{}) 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} } ``` WithValue只能通过添加的方式把父Ctx和新Ctx建立连接,很显然整个Ctx看起来应该就是一棵树一样。比如如下的代码 ```go type TrackId string func main() { ctx1 := context.WithValue(context.Background(), TrackId("2021"), "123456") ctx2 := context.WithValue(ctx1, TrackId("2020"), "111111") ctx3 := context.WithValue(ctx1, TrackId("2020"), "222222") ctx4 := context.WithValue(ctx2, TrackId("2019"), "333333") ctx5 := context.WithValue(ctx2, TrackId("2019"), "444444") ctx6 := context.WithValue(ctx3, TrackId("2018"), "555555") ctx7 := context.WithValue(ctx3, TrackId("2018"), "666666") var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("ctx4 ", ctx4.Value(TrackId("2019"))) fmt.Println("ctx5 ", ctx5.Value(TrackId("2019"))) fmt.Println("ctx6 ", ctx6.Value(TrackId("2020"))) fmt.Println("ctx7 ", ctx7.Value(TrackId("2021"))) }() wg.Wait() } ``` 其实就是会建立这样一棵树结构 ``` ┌────────────────────────┐ │ ┌********************┐ │ │ * Background * │ │ └********************┘ │ └────────────┬───────────┘ │ │ ┌────────────▼───────────┐ │ │ │ ctx1(val:123456) ◀──────Step3 │ │ └────────────┬───────────┘ │ ┌──────────────────────────┴──────────────────────────┐ │ │ │ │ ┌────────────▼───────────┐ ┌────────────▼───────────┐ │ │ │ │ │ ctx2(val:111111) │ │ ctx3(val:222222) ◀──────Step2 │ │ │ │ └────────────┬───────────┘ └────────────┬───────────┘ │ │ ┌─────────────┴────────────┐ ┌─────────────┴────────────┐ │ │ │ │ │ │ │ │ ┌────────────▼───────────┐ ┌────────────▼───────────┐ ┌────────────▼───────────┐ ┌────────────▼───────────┐ │ │ │ │ │ │ │ │ │ ctx4(val:333333) │ │ ctx5(val:444444) │ │ ctx6(val:555555) │ │ ctx7(val:666666) ◀──────Step1 │ │ │ │ │ │ │ │ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ``` 当然如果调用挂载的节点越多,这棵树就越大,而遍历这棵树找value信息就越慢,事实上上找value信息就是通过往上递归遍历的方法来查找的。 ```go func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } ``` 比如ctx7.Value(TrackId("2021"))就需要通过Step1,2,3才能找到最终的value:123456。 ### 总结 timerCtx,cancelCtx可以认为是管理goroutine生命周期的一类Ctx,另外valueCtx是传递参数作用的Ctx,使用的场景其实有区别,比较会误用的是valueCtx。这里摘取了一些使用中容易挖的坑,其实要使用好它,还真不容易! 标签: none
评论已关闭