教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang context源码学习

golang context源码学习

发布时间:2022-03-02   编辑:jiaochengji.com
教程集为您提供golang context源码学习等资源,欢迎您收藏本站,我们将为您提供最新的golang context源码学习资源
<h1>golang context源码学习</h1><h2>使用实例</h2>

context设置超时的例子

<pre><code class="lang-go hljs">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) } }</code></code></pre>

输出:

<pre><code class="lang-go hljs">main context deadline exceeded handle context deadline exceeded</code></code></pre>

Context设置的超时时间是1s,但是处理时间是1.5s,最后肯定会触发超时,在handle协程里,time.After会在程序启动1.5s之后返回消息,而ctx.Done()会在1s以后返回消息,time.After没有机会被捕获到,handle协程就退出了;而主协程同样也能捕获到ctx.Done里的消息。

<code>context的使用方法和设计原理</code>:多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

<h2>源码解析</h2><h3>context.Context接口</h3><pre><code class="lang-go hljs">type Context interface{ Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }</code></code></pre>

Context接口定了四个方法:

<ul><li>Deadline()会返回这个context终止时的时间以及是否被设置了超时。</li><li>Done()会返回一个只读管道,通常需要外部有select语句来监听这个管道,若是context被cancel掉,那么通过该接口能够获取到消息,没被cancel时,会一直阻塞在context.Done()的读取上。这里的cancel指WithCancel,WithDeadline,WithTimeout触发的cancel。</li><li>若ctx没被cancel掉,Err()只会返回nil,若被cancel掉则会返回为何被cancel,例如deadline。</li><li>Value()接口是用来存值的,在后续的context中可以将其取出。</li></ul><h3>context.Background(),context.TODO()</h3>

context.Background()和context.TODO()会返回一个相同的结构体,即emptyCtx。

<pre><code class="lang-go hljs">var ( background = new(emptyCtx) todo = new(emptyCtx) )</code></code></pre>

emptyCtx对Context接口的方法实现如下:

<pre><code class="lang-go hljs">// 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 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 interface{}) interface{} { return nil }</code></code></pre>

即它没有对Context进行任何的实现。

<ul><li>Background()返回的context通常作为整个context调用链的根context。</li><li>TODO()返回的context通常是在重构或编码过程中使用,不确定会如何使用</li></ul>

Context参数,但用其作为占位符,TODO的标识便于后续代码完成时能快速检查到这个未实现的占位符,以便将其实现。

<h3>context.WithCancel()</h3><pre><code class="lang-go hljs">func WithCancel(parent Context) (ctx Context, cancel CancelFunc)</code></code></pre>

从函数签名可以看出,WithCancel会返回一个设置了cancel的context和一个取消函数,这个返回的context的Done管道会被关闭,当父context的Done管道被关闭时或者取消函数被调用时。

<h4>创建一个cancelCtx</h4><pre><code class="lang-go hljs">c := newCancelCtx(parent)</code></code></pre><pre><code class="lang-go hljs">// newCancelCtx returns an initialized cancelCtx. func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }</code></code></pre>

看一下cancelCtx的结构:

<pre><code class="lang-go hljs">// 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 }</code></code></pre><h4>cancelCtx取消时,会将后代节点中所有的cancelCtx都取消,propagateCancel即用来建立当前节点与祖先节点这个取消关联逻辑。</h4><pre><code class="lang-go hljs">propagateCancel(parent, &c)</code></code></pre>

为啥需要建立与祖先的关联逻辑呢?后续会提到。

注意,propagateCancel的第二个参数是一个canceler接口,由定义可知:

<pre><code class="lang-go hljs">// 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{} }</code></code></pre>

这个接口由两种带有取消性质的ctx实现(cancelCtx和timerCtx)

<ul><li>第一个方法是cancel,由参数可以知它会将自身取消掉,若第一个参数为true,则会从父ctx中删除掉自己</li><li>第二个方法会直接返回一个Done管道</li></ul><pre><code class="lang-go hljs">// 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(): } }() } }</code></code></pre><ol><li>如果parent.Done()返回nil,表明父节点以上的路径上没有可取消的context,不需要处理;</li><li>如果在context链上找到到cancelCtx类型的祖先节点,则判断这个祖先节点是否已经取消,如果已经取消就取消当前节点;否则将当前节点加入到祖先节点的children列表。</li></ol>

否则开启一个协程,监听parent.Done()和child.Done(),一旦parent.Done()返回的channel关闭,即context链中某个祖先节点context被取消,则将当前context也取消。
这里或许有个疑问,为什么是祖先节点而不是父节点?这是因为当前context链可能是这样的:

<span class="img-wrap"></span>

当前cancelCtx的父节点context并不是一个可取消的context,也就没法记录children。这也是为什么需要在这个函数中建立当前节点与祖先cancelCtx节点的cancel关系

问题:
为什么会有

<pre><code class="lang-go hljs">go func() { select { case <-parent.Done(): //这种情况何时会出现? child.cancel(false, parent.Err()) case <-child.Done(): } }()</code></code></pre>

因为else中,已经说明了祖先ctx不为可取消的ctx,那为啥还能够捕获到第一个case的Done管道的信号呢?

这里需要引入parentCancelCtx的代码

<pre><code class="lang-go hljs">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 } } }</code></code></pre>

这里只能判断出cancelCtx,timeCtx,valueCtx三种类型,而若是将ctx内嵌到一个自定义的结构体a中,并且之后调用了WithCancel构建了子节点b。将这个子节点再调用WithCancel构建孙节点c,此时parentCancelCtx方法是识别不出这个b为cancelCtx的,因此需要使用else下的第一个case来捕获b节点的Done消息。

再来说一下,select 语句里的两个 case 其实都不能删。

<pre><code class="lang-go hljs">select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): }</code></code></pre><ul><li>第一个 case 说明当父节点取消,则取消子节点。如果去掉这个 case,那么父节点取消的信号就不能传递到子节点。</li><li>第二个 case 是说如果子节点自己取消了,那就退出这个 select,父节点的取消信号就不用管了。如果去掉这个 case,那么很可能父节点一直不取消,这个 goroutine 就泄漏了。当然,如果父节点取消了,就会重复让子节点取消,不过,这也没什么影响嘛。</li></ul><h4>返回一个cancelCtx与一个cancel函数</h4><pre><code class="lang-go hljs">return &c, func() { c.cancel(true, Canceled) }</code></code></pre>

这个cancel函数被外部调用时,会将自身从父节点中删除掉。并且cancel掉该节点的所有子节点:

<pre><code class="lang-go hljs">// 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) } }</code></code></pre>

两个问题需要回答:

<ol><li>什么时候会传 true?</li><li>为什么有时传 true,有时传 false?</li></ol>

答1:
外部调用cancel时传true,内部调用cancel时传false。
调用 WithCancel() 方法的时候,也就是新创建一个可取消的 context 节点时,返回的 cancelFunc 函数会传入 true。这样做的结果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点,你自己取消了,所以我要和你断绝关系,对其他人没影响

答2:
内部调用cancel时不用从父节点删除掉自身。内部调用的时机是:

<pre><code class="lang-go hljs">1. 在建立祖先与当前ctx的cancel关系时,若发现祖先已经被cancel了,这时会内部调用cancel:这种情况下,祖先通常是已经被外部调用了cancel,它已经将其chidren置为了nil,这时就不必要再删除了; 2. 监听到祖先的Done管道关闭,即祖先已经被cancel掉,这种情况和第一种情况类似,children已经被置为了nil,不必要再删除。 </code></code></pre><h3>context.WithTimeout()</h3><pre><code class="lang-go hljs">func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }</code></code></pre>

context.WithTimeout方法底层其实是调用的WithDeadline,返回了一个带有 超时信息的context和一个取消函数,标准用法如下:

<pre><code class="lang-go hljs">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) }</code></code></pre>

看一下WithDeadline的实现方式

<pre><code class="lang-go hljs">func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)</code></code></pre>

由函数签名可以看出会返回一个带超时的context和一个取消函数,若WithDeadline返回的context在d时间点未被取消,那么它的Done管道将被强制关闭。

具体实现代码:

<h4>判断父context是否已经超时了:</h4><pre><code class="lang-go hljs">if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }</code></code></pre>

若是父context被设置了超时,且截止时间点要短于当前期望设置的超时时间的截止时间点,那么直接基于父ctx构建一个可取消的ctx。
原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。因此构建一个绝对时间晚于父节点的子ctx是没有意义的。

<h4>构建timerCtx</h4>

首先需要了解一下timerCtx:

<pre><code class="lang-go hljs">// 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 }</code></code></pre>

可以看到timerCtx内嵌了cancelCtx,同时它还有自己的timer和deadline。
Timer是一个定时器,当到达设定的时间后,会向timer的管道中发一个事件。
会在 deadline 到来时,会监听到事件,这时自动取消 context,这是在ctx的WithDeadline中做的。

看下timerCtx的cancel方法

<pre><code class="lang-go hljs">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() }</code></code></pre><ol><li>首先会将自己和所有子节点cancel掉;</li><li>会将自身的timer的Stop掉,防止deadline到来时再次被取消。</li></ol><h4>构建当前ctx与祖先ctx的cancel关系</h4><pre><code class="lang-go hljs">propagateCancel(parent, c)</code></code></pre>

这个与之前的cancel是一样的,不再赘述。

<h4>计算当前时间与deadline的时间差</h4><pre><code class="lang-go hljs">dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } }</code></code></pre>

如果当前时间已经达到了deadline的时间点,那么直接将其取消,并返回。

<h4>设置timer到期的回调函数</h4><pre><code class="lang-go hljs">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) }</code></code></pre>

如果timer到期后timerCtx会调用cancel取消自己。

<h2>参考文章</h2>

https://www.zhihu.com/search?...

到此这篇关于“ golang context源码学习”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang context源码学习
jQuery.prototype.init选择器构造函数源码思路分析
jquery源码-核心源码结构
golang goroutine 通知_深入golang之---goroutine并发控制与通信
理解 golang 中的 context(上下文) 包
golang int 转 duration_一看就懂系列之Golang的context
golang 上下文_Golang中的上下文!
深入理解Golang之Context(可用于实现超时机制)
想系统学习GO语言(Golang
javascript数据缓存至本地的实例参考

[关闭]
~ ~