golang context源码学习
应用实例
context设置超时的例子
func main() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() go handle(ctx, 1500*time.Millisecond) select { case <-ctx.Done(): fmt.Println("main", ctx.Err()) } time.Sleep(3 * time.Second)}func handle(ctx context.Context, duration time.Duration) { select { case <-ctx.Done(): fmt.Println("handle", ctx.Err()) case <-time.After(duration): fmt.Println("process request with", duration) }}
输入:
main context deadline exceededhandle context deadline exceeded
Context设置的超时工夫是1s,然而解决工夫是1.5s,最初必定会触发超时,在handle协程里,time.After会在程序启动1.5s之后返回音讯,而ctx.Done()会在1s当前返回音讯,time.After没有机会被捕捉到,handle协程就退出了;而主协程同样也能捕捉到ctx.Done里的音讯。
context的应用办法和设计原理
:多个 Goroutine 同时订阅 ctx.Done() 管道中的音讯,一旦接管到勾销信号就立即进行以后正在执行的工作。
源码解析
context.Context接口
type Context interface{ Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Context接口定了四个办法:
- Deadline()会返回这个context终止时的工夫以及是否被设置了超时。
- Done()会返回一个只读管道,通常须要内部有select语句来监听这个管道,若是context被cancel掉,那么通过该接口可能获取到音讯,没被cancel时,会始终阻塞在context.Done()的读取上。这里的cancel指WithCancel,WithDeadline,WithTimeout触发的cancel。
- 若ctx没被cancel掉,Err()只会返回nil,若被cancel掉则会返回为何被cancel,例如deadline。
- Value()接口是用来存值的,在后续的context中能够将其取出。
context.Background(),context.TODO()
context.Background()和context.TODO()会返回一个雷同的构造体,即emptyCtx。
var ( background = new(emptyCtx) todo = new(emptyCtx))
emptyCtx对Context接口的办法实现如下:
// An emptyCtx is never canceled, has no values, and has no deadline. It is not// struct{}, since vars of this type must have distinct addresses.type emptyCtx intfunc (*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}
即它没有对Context进行任何的实现。
- Background()返回的context通常作为整个context调用链的根context。
- TODO()返回的context通常是在重构或编码过程中应用,不确定会如何应用
Context参数,但用其作为占位符,TODO的标识便于后续代码实现时能疾速查看到这个未实现的占位符,以便将其实现。
context.WithCancel()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
从函数签名能够看出,WithCancel会返回一个设置了cancel的context和一个勾销函数,这个返回的context的Done管道会被敞开,当父context的Done管道被敞开时或者勾销函数被调用时。
创立一个cancelCtx
c := newCancelCtx(parent)
// newCancelCtx returns an initialized cancelCtx.func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent}}
看一下cancelCtx的构造:
// A cancelCtx can be canceled. When canceled, it also cancels any children// that implement canceler.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}
cancelCtx勾销时,会将后辈节点中所有的cancelCtx都勾销,propagateCancel即用来建设以后节点与先人节点这个勾销关联逻辑。
propagateCancel(parent, &c)
为啥须要建设与先人的关联逻辑呢?后续会提到。
留神,propagateCancel的第二个参数是一个canceler接口,由定义可知:
// A canceler is a context type that can be canceled directly. The// implementations are *cancelCtx and *timerCtx.type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{}}
这个接口由两种带有勾销性质的ctx实现(cancelCtx和timerCtx)
- 第一个办法是cancel,由参数能够知它会将本身勾销掉,若第一个参数为true,则会从父ctx中删除掉本人
- 第二个办法会间接返回一个Done管道
// propagateCancel arranges for child to be canceled when parent is.func propagateCancel(parent Context, child canceler) { if parent.Done() == nil { return // parent is never canceled } //如果parent的Done管道是空,阐明该父context永远不会被勾销,通常是emptyCtx,那么不必建设该ctx与先人ctx的关系 if p, ok := parentCancelCtx(parent); ok {//当其父context为cancel性质的context(timerCtx或cancelCtx)会返回true和这个cancelCtx,若为valueCtx则会由context向上查找直到找到一个cancel性质的ctx;否则返回false //父ctx为cancel性质的ctx //加锁 p.mu.Lock() if p.err != nil { // parent has already been canceled //父ctx曾经被cancel掉了,所以该子ctx将会间接调用cancel办法cancel掉本人。 //这样,尽管之后给内部调用返回了一个cancel函数,然而因为在child.cancel中曾经设置了c.err,所以之后内部再调用cancel,cancelCtx的cancel办法也不会再做别的操作了,发现c.err不为nil,间接return,代表曾经被cancel掉了 child.cancel(false, p.err) } else { //否则把本人退出到先人cancelCtx的children中 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { //父context不为cancelCtx,则起一个后盾协程,监听父ctx和以后ctx的Done管道,直到有ctx的Done管道被敞开(ctx被勾销) //TODO,为何parent不为cancelCtx,还能有done信号被捕捉到??? go func() { select { case <-parent.Done(): //这种状况何时会呈现? child.cancel(false, parent.Err()) case <-child.Done(): } }() }}
- 如果parent.Done()返回nil,表明父节点以上的门路上没有可勾销的context,不须要解决;
- 如果在context链上找到到cancelCtx类型的先人节点,则判断这个先人节点是否曾经勾销,如果曾经勾销就勾销以后节点;否则将以后节点退出到先人节点的children列表。
否则开启一个协程,监听parent.Done()和child.Done(),一旦parent.Done()返回的channel敞开,即context链中某个先人节点context被勾销,则将以后context也勾销。
这里或者有个疑难,为什么是先人节点而不是父节点?这是因为以后context链可能是这样的:
以后cancelCtx的父节点context并不是一个可勾销的context,也就没法记录children。这也是为什么须要在这个函数中建设以后节点与先人cancelCtx节点的cancel关系。
问题:
为什么会有
go func() { select { case <-parent.Done(): //这种状况何时会呈现? child.cancel(false, parent.Err()) case <-child.Done(): } }()
因为else中,曾经阐明了先人ctx不为可勾销的ctx,那为啥还可能捕捉到第一个case的Done管道的信号呢?
这里须要引入parentCancelCtx的代码
func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true case *timerCtx: return &c.cancelCtx, true case *valueCtx: parent = c.Context default: return nil, false } }}
这里只能判断出cancelCtx,timeCtx,valueCtx三种类型,而若是将ctx内嵌到一个自定义的构造体a中,并且之后调用了WithCancel构建了子节点b。将这个子节点再调用WithCancel构建孙节点c,此时parentCancelCtx办法是辨认不出这个b为cancelCtx的,因而须要应用else下的第一个case来捕捉b节点的Done音讯。
再来说一下,select 语句里的两个 case 其实都不能删。
select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done():}
- 第一个 case 阐明当父节点勾销,则勾销子节点。如果去掉这个 case,那么父节点勾销的信号就不能传递到子节点。
- 第二个 case 是说如果子节点本人勾销了,那就退出这个 select,父节点的勾销信号就不必管了。如果去掉这个 case,那么很可能父节点始终不勾销,这个 goroutine 就透露了。当然,如果父节点勾销了,就会反复让子节点勾销,不过,这也没什么影响嘛。
返回一个cancelCtx与一个cancel函数
return &c, func() { c.cancel(true, Canceled) }
这个cancel函数被内部调用时,会将本身从父节点中删除掉。并且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 if c.done == nil { //惰性创立,初始化时不会赋值。须要勾销时间接给一个敞开的channel //很有意思的是这个channel是在init里被敞开的 c.done = closedchan } else { close(c.done) } //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() //何时该参数应该为true? //当内部调用cancel时,该参数为true if removeFromParent { removeChild(c.Context, c) }}
两个问题须要答复:
- 什么时候会传 true?
- 为什么有时传 true,有时传 false?
答1:
内部调用cancel时传true,外部调用cancel时传false。
调用 WithCancel() 办法的时候,也就是新创建一个可勾销的 context 节点时,返回的 cancelFunc 函数会传入 true。这样做的后果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点,你本人勾销了,所以我要和你断绝关系,对其他人没影响
答2:
外部调用cancel时不必从父节点删除掉本身。外部调用的机会是:
1. 在建设先人与以后ctx的cancel关系时,若发现先人曾经被cancel了,这时会外部调用cancel:这种状况下,先人通常是曾经被内部调用了cancel,它曾经将其chidren置为了nil,这时就不必要再删除了;2. 监听到先人的Done管道敞开,即先人曾经被cancel掉,这种状况和第一种状况相似,children曾经被置为了nil,不必要再删除。
context.WithTimeout()
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}
context.WithTimeout办法底层其实是调用的WithDeadline,返回了一个带有 超时信息的context和一个勾销函数,规范用法如下:
func slowOperationWithTimeout(ctx context.Context) (Result, error) { ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() // releases resources if slowOperation completes before timeout elapses return slowOperation(ctx)}
看一下WithDeadline的实现形式
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
由函数签名能够看出会返回一个带超时的context和一个勾销函数,若WithDeadline返回的context在d工夫点未被勾销,那么它的Done管道将被强制敞开。
具体实现代码:
判断父context是否曾经超时了:
if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }
若是父context被设置了超时,且截止工夫点要短于以后冀望设置的超时工夫的截止工夫点,那么间接基于父ctx构建一个可勾销的ctx。
起因是一旦父节点超时,主动调用 cancel 函数,子节点也会随之勾销。因而构建一个相对工夫晚于父节点的子ctx是没有意义的。
构建timerCtx
首先须要理解一下timerCtx:
// 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 timer *time.Timer // Under cancelCtx.mu. deadline time.Time}
能够看到timerCtx内嵌了cancelCtx,同时它还有本人的timer和deadline。
Timer是一个定时器,当达到设定的工夫后,会向timer的管道中发一个事件。
会在 deadline 到来时,会监听到事件,这时主动勾销 context,这是在ctx的WithDeadline中做的。
看下timerCtx的cancel办法
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()}
- 首先会将本人和所有子节点cancel掉;
- 会将本身的timer的Stop掉,避免deadline到来时再次被勾销。
构建以后ctx与先人ctx的cancel关系
propagateCancel(parent, c)
这个与之前的cancel是一样的,不再赘述。
计算以后工夫与deadline的时间差
dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } }
如果以后工夫曾经达到了deadline的工夫点,那么间接将其勾销,并返回。
设置timer到期的回调函数
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) }
如果timer到期后timerCtx会调用cancel勾销本人。
参考文章
https://www.zhihu.com/search?...