游弋的鱼 發表於 2021-1-14 14:21:00

深入理解Go Context

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>emptyCtx类型</li><li>cancelCtx类型</li><li>timerCtx类型</li><li>valueCtx类型</li></ul></div><p></p>
<p>在Go语言并发编程中,用一个goroutine来处理一个任务,而它又会创建多个goroutine来负责不同子任务的场景非常常见。如下图</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141751483-884839201.png" alt="" loading="lazy"></p>
<p>这些场景中,往往会需要在API边界之间以及过程之间传递截止时间、取消信号或与其它请求相关的数据</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141825980-1951960722.png" alt="" loading="lazy"></p>
<p>谁是性能卡点呢?得通知它们任务取消了。</p>
<p>这时候就可以使用<code>Context</code>了。context包在Go1.7的时候被加入到官方库中。</p>
<p>context包的内容可以概括为,一个接口,四个具体实现,还有六个函数。</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141813698-554893972.png" alt="" loading="lazy"></p>
<p>Context接口提供了四个方法,下面是Context的接口</p>
<pre><code class="language-go">type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() &lt;-chan struct{}
        Err() error
        Value(key interface{}) interface{}
}
</code></pre>
<h2 id="emptyctx类型">emptyCtx类型</h2>
<p>emptyCtx本质上是一个整型, *emptyCtx对Context接口的实现,只是简单的返回nil,false,实际上什么也没做。如下代码所示:</p>
<pre><code class="language-go">type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
        return
}

func (*emptyCtx) Done() &lt;-chan struct{} {
        return nil
}

func (*emptyCtx) Err() error {
        return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
        return nil
}
</code></pre>
<p>Background和TODO这两个函数内部都会创建emptyCtx</p>
<pre><code class="language-go">var (
        background = new(emptyCtx)
        todo       = new(emptyCtx)
)

func Background() Context {
        return background
}

func TODO() Context {
        return todo
}
</code></pre>
<p>其中Background主要用于在初始化时获取一个Context(从代码中可知本质是一个*emptyCtx,而emptCtx本质上是一个Int),这就是Background()函数返回的变量结构。</p>
<p>而TODO()函数,官方文档建议在本来应该使用外层传递的ctx而外层却没有传递的地方使用,就像函数名称表达的含义一样,留下一个TODO。</p>
<h2 id="cancelctx类型">cancelCtx类型</h2>
<p>再来看cancelCtx类型,cancleCtx定义如下</p>
<pre><code class="language-go">// cancelCtx可以被取消。 取消后,它也会取消所有实现取消方法的子级。
type cancelCtx struct {
        Context

        mu       sync.Mutex            // protects following fields
        done   chan struct{}         // created lazily, closed by first cancel call
        children mapstruct{} // set to nil by the first cancel call
        err      error               // set to non-nil by the first cancel call
}

func (c *cancelCtx) Value(key interface{}) interface{} {
        if key == &amp;cancelCtxKey {
                return c
        }
        return c.Context.Value(key)
}

func (c *cancelCtx) Done() &lt;-chan struct{} {
        c.mu.Lock()
        if c.done == nil {
                c.done = make(chan struct{})
        }
        d := c.done
        c.mu.Unlock()
        return d
}

func (c *cancelCtx) Err() error {
        c.mu.Lock()
        err := c.err
        c.mu.Unlock()
        return err
}
</code></pre>
<p>这是一种可取消的Context,done用于获取该Context的取消通知,children用于存储以当前节点为根节点的所有可取消的Context,以便在根节点取消时,可以把它们一并取消,err用于存储取消时指定的错误信息,而这个mu就是用来保护这几个字段的锁,以保障cancelCtx是线程安全的。</p>
<p>而WithCancel函数,可以把一个Context包装为cancelCtx,并提供一个取消函数,调用它可以Cancel对应的Context</p>
<pre><code class="language-go">func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        c := newCancelCtx(parent)
        propagateCancel(parent, &amp;c)
        return &amp;c, func() { c.cancel(true, Canceled) }
}
</code></pre>
<p>示例代码:</p>
<pre><code class="language-go">ctx := context.Background()
ctx1, cancel := context.WithCancel(ctx)
</code></pre>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141902131-1957228022.png" alt="" loading="lazy"></p>
<h2 id="timerctx类型">timerCtx类型</h2>
<p>再来看timerCtx,timerCtx定义如下</p>
<pre><code class="language-go">type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.

        deadline time.Time
}
</code></pre>
<p>它在cancelCtx的基础上,又封装了一个定时器和一个截止时间,这样既可以根据需要主动取消,也可以在到达deadline时,通过timer来触发取消动作。</p>
<p>要注意,这个timer也会由cancelCtx.mu来保护,确保取消操作也是线程安全的。</p>
<p>通过WithDeadline和WithTimeout函数,都可以创建timerCtx,区别是WithDeadline函数需要指定一个时间点,而WithTimeout函数接收一个时间段。</p>
<p>接下来,我们基于ctx1构造一个timerCtx</p>
<pre><code class="language-go">ctx := context.Background()
ctx1, cancel := context.WithCancel(ctx)

deadline := time.Now().Add(time.Second)
ctx2, cancel := context.WithDeadline(ctx1, deadline)
</code></pre>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141914061-488782751.png" alt="" loading="lazy"></p>
<p>这个定时器会在deadline到达时,调用cancelCtx的取消函数,现在可以看到ctx2是基于ctx1创建的,而ctx1又是基于ctx创建的,基于每个Context可以创建多个Context,这样就形成了一个Context树,每个节点都可以有零个或多个子节点,可取消的Context都会被注册到离它最近的、可取消的祖先节点中。对ctx2来说离它最新的、可取消的祖先节点是ctx1</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141922330-1299247475.png" alt="" loading="lazy"></p>
<p>所以在ctx1这里的children map中,会增加ctx2这组键值对</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141946356-49764307.png" alt="" loading="lazy"></p>
<p>如果ctx2先取消,就只会影响到以它为根节点的Context,而如果ctx1先取消,就可以根据children map中的记录,把ctx1子节点中所有可取消的Context全部Cancel掉。</p>
<p>最后来看valueCtx类型</p>
<h2 id="valuectx类型">valueCtx类型</h2>
<p>首先来看valueCtx的定义</p>
<pre><code class="language-go">type valueCtx struct {
        Context
        key, val interface{}
}
</code></pre>
<p>它用来支持键值对打包,WithValue函数可以给Context附加一个键值对信息,这样就可以通过Context传递数据了</p>
<pre><code class="language-go">var keyA string = "keyA"
ctx := context.Background()
ctxA := context.WithValue(ctx, keyA, "valA")
</code></pre>
<p>现在我们给ctx附加一个键值对keyA=&gt;valA,变量ctxA也是Context接口类型,动态类型为*valueCtx,data指向一个valueCtx结构体,第一个字段是它的父级Context,key和val字段都是空接口类型,keyA的动态类型为string,动态值是string类型的变量keyA,val的动态类型同样是string,动态值为valA,</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114141956662-1690758359.png" alt="" loading="lazy"></p>
<p>下面我们再基于ctxA,附加一个key相等但val不相等的键值对keyA=&gt;eggo,ctxC的动态值指向这样一个valueCtx,父级Context自然是ctxA,key与ctxA中的相同,但是val的值与ctxA中的不相等</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114142004572-515925435.png" alt="" loading="lazy"></p>
<p>通过ctxC获取kyA和keyC对应的值时会发现keyC覆盖了keyA对应的val,要找到原因,就要先看看Value方法是怎么工作的</p>
<pre><code class="language-go">func (c *valueCtx) Value(key interface{}) interface{} {
        if c.key == key {
                return c.val
        }
        return c.Context.Value(key)
}
</code></pre>
<p>首先它会比较当前Context中的key是否等于要查找的key,因为keyA等于keyC,所以对keyA的查找会直接锁定到ctxC这里的val,因而出现了子节点覆盖父节点数据的情况,为了规避这种情况,最好不要直接使用string、int这些基础类型作为Key,而是用自定义类型包装一下,就像下面这样,把keyA定义为keytypea类型,keyC定义为keytypec类型,这样再次通过ctxC获取keyA时,因为key的类型不相同,第一步key相等性比较不通过,就会委托父节点继续查找,进而找到正确的val</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114142015342-1043525441.png" alt="" loading="lazy"></p>
<p>所以说valueCtx之间通过Context字段形成了一个链表结构,使用Context传递数据时还要注意,Context本身本着不可改变(immutable)的模式设计的,所以不要试图修改ctx里保存的值,在http、sql相关的库中,都提供了对Context的支持,方便我们在处理请求时,实现超时自动取消,或传递请求相关的控制数据等等</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114142021476-1290724011.png" alt="" loading="lazy"></p>
<p>了解了context包中,一个接口,四种具体实现,以及六个函数的基本情况,有助于我们理解Context的工作原理</p>
<p><img src="https://img2020.cnblogs.com/blog/720430/202101/720430-20210114142028388-1351873889.png" alt="" loading="lazy"></p>
<p>整理自:</p>
<p>context源码</p>
<p>幼麟实验室</p><br><br>
来源:https://www.cnblogs.com/itbsl/p/14277002.html
頁: [1]
查看完整版本: 深入理解Go Context