鹿鹿就是梦妍 發表於 2021-2-24 10:41:00

go context详解

<p>Context通常被称为上下文,在go中,理解为goroutine的运行状态、现场,存在上下层goroutine context的传递,上层goroutine会把context传递给下层goroutine。</p>
<p>每个goroutine在运行前,都要事先知道程序当前的执行状态,通常将这些状态封装在一个 context变量,传递给要执行的goroutine中。</p>
<p>在网络编程中,当接收到一个网络请求的request,处理request时,可能会在多个goroutine中处理。而这些goroutine可能需要共享Request的一些信息;当request被取消或者超时时,所有从这个request创建的goroutine也要被结束。</p>
<p>&nbsp;</p>
<p>go context包不仅实现了在程序单元之间共享状态变量的方法,同时能通过简单的方法,在被调用程序单元外部,通过设置ctx变量的值,将过期或撤销等信号传递给被调用的程序单元。在网络编程中,如果存在A调用B的API,B调用C的 API,如果A调用B取消,那么B调用C也应该被取消,通过在A、B、C调用之间传递context,以及判断其状态,就能解决此问题。</p>
<p>&nbsp;</p>
<p>通过context包,可以非常方便地在请求goroutine之间传递请求数据、取消信号和超时信息。</p>
<p>&nbsp;</p>
<p>context包的核心时Context接口</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
        // Deadline returns the time when work done on behalf of this context
        // should be canceled. Deadline returns ok==false when no deadline is
        // set. Successive calls to Deadline return the same results.<br>      // 返回一个超时时间,到期则取消context。在代码中,可以通过 deadline 为io操作设置超时时间
        Deadline() (deadline time.Time, ok bool)

        // Done returns a channel that's closed when work done on behalf of this
        // context should be canceled. Done may return nil if this context can
        // never be canceled. Successive calls to Done return the same value.
        // The close of the Done channel may happen asynchronously,
        // after the cancel function returns.
        //
        // WithCancel arranges for Done to be closed when cancel is called;
        // WithDeadline arranges for Done to be closed when the deadline
        // expires; WithTimeout arranges for Done to be closed when the timeout
        // elapses.
        //
        // Done is provided for use in select statements:
        //
        //// Stream generates values with DoSomething and sends them to out
        //// until DoSomething returns an error or ctx.Done is closed.
        //func Stream(ctx context.Context, out chan&lt;- Value) error {
        //        for {
        //                v, err := DoSomething(ctx)
        //                if err != nil {
        //                        return err
        //                }
        //                select {
        //                case &lt;-ctx.Done():
        //                        return ctx.Err()
        //                case out &lt;- v:
        //                }
        //        }
        //}
        //
        // See https://blog.golang.org/pipelines for more examples of how to use
        // a Done channel for cancellation.<br>     // 返回一个channel, 用于接收context的取消或者deadline信号。当channel关闭,监听done信号的函数会立即放弃当前正在执行的操作并返回。如果 context实例是不可取消的,那么<br>      // 返回 nil, 比如空 context, valueCtx
        Done() &lt;-chan struct{}

        // If Done is not yet closed, Err returns nil.
        // If Done is closed, Err returns a non-nil error explaining why:
        // Canceled if the context was canceled
        // or DeadlineExceeded if the context's deadline passed.
        // After Err returns a non-nil error, successive calls to Err return the same error.<br>     // 返回一个error变量,从其中可以知道为什么context会被取消。
        Err() error

        // Value returns the value associated with this context for key, or nil
        // if no value is associated with key. Successive calls to Value with
        // the same key returns the same result.
        //
        // Use context values only for request-scoped data that transits
        // processes and API boundaries, not for passing optional parameters to
        // functions.
        //
        // A key identifies a specific value in a Context. Functions that wish
        // to store values in Context typically allocate a key in a global
        // variable then use that key as the argument to context.WithValue and
        // Context.Value. A key can be any type that supports equality;
        // packages should define keys as an unexported type to avoid
        // collisions.
        //
        // Packages that define a Context key should provide type-safe accessors
        // for the values stored using that key:
        //
        //         // Package user defines a User type that's stored in Contexts.
        //         package user
        //
        //         import "context"
        //
        //         // User is the type of value stored in the Contexts.
        //         type User struct {...}
        //
        //         // key is an unexported type for keys defined in this package.
        //         // This prevents collisions with keys defined in other packages.
        //         type key int
        //
        //         // userKey is the key for user.User values in Contexts. It is
        //         // unexported; clients use user.NewContext and user.FromContext
        //         // instead of using this key directly.
        //         var userKey key
        //
        //         // NewContext returns a new Context that carries value u.
        //         func NewContext(ctx context.Context, u *User) context.Context {
        //                 return context.WithValue(ctx, userKey, u)
        //         }
        //
        //         // FromContext returns the User value stored in ctx, if any.
        //         func FromContext(ctx context.Context) (*User, bool) {
        //                 u, ok := ctx.Value(userKey).(*User)
        //                 return u, ok
        //         }<br>     // 让context在goroutine之间共享数据,当然,这些数据需要时协程并发安全的。比如,共享了一个map,那么这个map的读写要加锁。
        Value(key interface{}) interface{}
}
</pre>
</div>
<p>  </p>
<p>context的使用:</p>
<p>对于goroutine,他们的创建和调用关系总是像层层调用进行的,就像一个树状结构,而更靠顶部的context应该有办法主动关闭下属的goroutine的执行。为了实现这种关系,context也是一个树状结构,叶子节点总是由根节点衍生出来的。</p>
<p>要创建context树,第一步应该得到根节点,context.Backupgroup函数的返回值就是根节点。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">// 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
}</pre>
</div>
<p>该函数返回空的context,该context一般由接收请求的第一个goroutine创建,是与进入请求对应的context根节点,他不能被取消,也没有值,也没有过期时间。他常常作为处理request的顶层的context存在。</p>
<p>有了根节点,就可以创建子孙节点了,context包提供了一系列方法来创建他们:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
func WithValue(parent Context, key, val interface{}) Context {}
</pre>
</div>
<p>  </p>
<p>函数都接收一个Context类型的parent,并返回一个context类型的值,这样就层层创建除不同的context,子节点是从复制父节点得到,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的goroutine了。</p>
<p>&nbsp;</p>
<p>怎么通过context传递改变后的状态呢?</p>
<p>在父goroutine中可以通过Withxx方法获取一个cancel方法,从而获得了操作子context的权力。</p>
<p>&nbsp;</p>
<p>WithCancel函数,是将父节点复制到子节点,并且返回一个额外的CancelFunc函数类型变量,该函数类型的定义为:type CancelFunc func()</p>
<div class="cnblogs_Highlighter">
<pre>type cancelCtx struct {<br>   Context<br>   mu       sync.Mutex            // protects following fields<br>   done   chan struct{}         // created lazily, closed by first cancel call<br>   children mapstruct{} // set to nil by the first cancel call<br>   err      error               // set to non-nil by the first cancel call<br>}</pre>
<pre>// 懒汉式创建,只有在调用Done()方法时,才会创建;该函数返回的是一个只读的 chan,没有地方向这个chan中写数据, 直接读取协程会被block住;所以一般搭配select来使用;一旦关闭,会立即读取出零值。<br>func (c *cancelCtx) Done() &lt;-chan struct{} {<br>   c.mu.Lock()<br>   if c.done == nil {<br>      c.done = make(chan struct{})<br>   }<br>   d := c.done<br>   c.mu.Unlock()<br>   return d<br>}<br><br></pre>
<pre>func (c *cancelCtx) cancel(removeFromParent bool, err error) {<br>   if err == nil {<br>      panic("context: internal error: missing cancel error")<br>   }<br>   c.mu.Lock()<br>   // 已经被其他协程取消<br>   if c.err != nil {<br>      c.mu.Unlock()<br>      return // already canceled<br>   }<br>   c.err = err<br>   // 关闭channel,通知其他协程<br>   if c.done == nil {<br>      c.done = closedchan<br>   } else {<br>      close(c.done)<br>   }<br>   // 遍历他的所有子节点 children, <br>   for child := range c.children {<br>      // NOTE: acquiring the child's lock while holding parent's lock.<br>      // 递归的取消所有子 ctx<br>      child.cancel(false, err)<br>   }<br>   c.children = nil<br>   c.mu.Unlock()<br><br>   if removeFromParent {<br>      // 从父ctx中移除自己<br>      removeChild(c.Context, c)<br>   }<br>}</pre>
<pre class="brush:go;gutter:true;"><br>func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)<br>      // 传播取消行为,根据 parent的情况,进行cancel还是将c添加的parent的children中
        propagateCancel(parent, &amp;c)
        return &amp;c, func() { c.cancel(true, Canceled) }
}<br><br></pre>
<pre>// 父context是否是可取消的context<br>func parentCancelCtx(parent Context) (*cancelCtx, bool) {<br>   done := parent.Done()<br>   if done == closedchan || done == nil {<br>      return nil, false<br>   }<br>   // 如果是 cancelCtx 或者 timerCtx, 则返回 parent,true; 否则返回 nil, false<br>   p, ok := parent.Value(&amp;cancelCtxKey).(*cancelCtx)<br>   if !ok {<br>      return nil, false<br>   }<br>   p.mu.Lock()<br>   ok = p.done == done<br>   p.mu.Unlock()<br>   // <br>   if !ok {<br>      return nil, false<br>   }<br>   return p, true<br>}}</pre>
<pre>// propagateCancel arranges for child to be canceled when parent is.<br>// 把child ctx cancel()关联到 parent节点上<br>func propagateCancel(parent Context, child canceler) {<br>   done := parent.Done()<br>   // 如果父类 context 不可取消,直接return<br>   if done == nil {<br>      return // parent is never canceled<br>   }<br>   select {<br>   case &lt;-done:<br>      // parent is already canceled<br>      // 父类context已经canceled, child直接cancel()<br>      child.cancel(false, parent.Err())<br>      return<br>   default:<br>   }<br><br>   // parent是否是可取消的cancelContext, 如果是,则挂靠上去<br>   if p, ok := parentCancelCtx(parent); ok {<br>      // 如果有<br>      p.mu.Lock()<br>      // err != nil,说明挂靠的parent已经被关闭,child直接cancel()<br>      if p.err != nil {<br>         // parent has already been canceled<br>         child.cancel(false, p.err)<br>      } else {<br>         // err == nil ,挂靠的parent没有被关闭 ;将child放入挂靠的parent的children数组中<br>         if p.children == nil {<br>            p.children = make(mapstruct{})<br>         }<br>         p.children = struct{}{}<br>      }<br>      p.mu.Unlock()<br>   } else {<br>      // 走到这里,说明树上没有cancelCtx<br>      atomic.AddInt32(&amp;goroutines, +1)<br>      // 新起一个goruntine<br>      go func() {<br>         select {<br>         case &lt;-parent.Done():<br>            // 如果收到取消信号,child cancel<br>            child.cancel(false, parent.Err())<br>         case &lt;-child.Done():<br>         }<br>      }()<br>   }<br>}</pre>
</div>
<p>  </p>
<p>调用 CancelFunc 将撤销对应的子context对象。在父goroutine中,通过 WithCancel 可以创建子节点的 Context, 还获得了子goroutine的控制权,一旦执行了 CancelFunc函数,子节点Context就结束了,子节点需要如下代码来判断是否已经结束,并退出goroutine:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">select {
case &lt;- ctx.Done():
        fmt.Println("do some clean work ...... ")
}
</pre>
</div>
<p>&nbsp;</p>
<p>WithDeadline函数作用和WithCancel差不多,也是将父节点复制到子节点,但是其过期时间是由deadline和parent的过期时间共同决定。当parent的过期时间早于deadline时,返回的过期时间与parent的过期时间相同。父节点过期时,所有的子孙节点必须同时关闭。</p>
<p>&nbsp;</p>
<p>WithTimeout函数和WithDeadline类似,只不过,他传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权,一个CancelFunc类型的函数变量。</p>
<p>当顶层的Request请求函数结束时,我们可以cancel掉某个context,而子孙的goroutine根据select ctx.Done()来判断结束。</p>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">// 使用WithDeadline 和 WithTimeout 都会生成一个 timerCtx, WithTimeout就是用 WithDeadline实现的。</pre>
<pre class="brush:go;gutter:true;">type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.
        deadline time.Time
}<br><br></pre>
<pre><br>func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {<br>   // 如果父节点的deadline更靠前,那么该 d就可以丢弃,使用父节点的 deadline<br>   if cur, ok := parent.Deadline(); ok &amp;&amp; cur.Before(d) {<br>      // The current deadline is already sooner than the new one.<br>      return WithCancel(parent)<br>   }<br>   c := &amp;timerCtx{<br>      cancelCtx: newCancelCtx(parent),<br>      deadline:d,<br>   }<br>   // 把当前 c节点ctx cancel函数关联到 parent节点上<br>   propagateCancel(parent, c)<br>   // 获取到 d的时间<br>   dur := time.Until(d)<br>   if dur &lt;= 0 {<br>      // 已经超时了,退出<br>      c.cancel(true, DeadlineExceeded) // deadline has already passed<br>      return c, func() { c.cancel(false, Canceled) }<br>   }<br>   c.mu.Lock()<br>   defer c.mu.Unlock()<br>   // parent节点到现在还没有取消<br>   if c.err == nil {<br>      // 到时间,自动退出<br>      c.timer = time.AfterFunc(dur, func() {<br>         c.cancel(true, DeadlineExceeded)<br>      })<br>   }<br>   return c, func() { c.cancel(true, Canceled) }<br>}</pre>
</div>
<p>  </p>
<p>WithValue函数,返回parent的一个副本,调用该副本的Value(key) 方法将得到value。这样,我们不仅将根节点原有的值保留了, 还在子孙节点中加入了新的值;注意如果存在key相同,则会覆盖。</p>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func WithValue(parent Context, key, val interface{}) Context {<br>      // key必须为非空,且可比较
        if key == nil {
                panic("nil key")
        }
        if !reflectlite.TypeOf(key).Comparable() {
                panic("key is not comparable")
        }
        return &amp;valueCtx{parent, key, val}
}<br><br></pre>
<pre>func (c *valueCtx) Value(key interface{}) interface{} {<br>   if c.key == key {<br>      return c.val<br>   }<br>   // 这里使用递归,c.Context就是 c.Parent<br>   return c.Context.Value(key)<br>}</pre>
</div>
<p>  </p>
<p>小结:</p>
<p>1. context包通过构建树形关系的context,来达到上一层goroutine对下一层goroutine的控制。对于处理一个request请求操作,需要通过goroutine来层层控制goroutine,以及传递一些变量来共享。</p>
<p>2. context变量的请求周期一般为一个请求的处理周期。即针对一个请求创建context对象;在请求处理结束后,撤销此ctx变量,释放资源。</p>
<p>3. 每创建一个goroutine,要不将原有context传递给子goroutine,要么创建一个子context传递给goroutine.</p>
<p>4.&nbsp;Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。</p>
<p>5. 当通过父 Context对象创建子Context时,可以同时获得子Context的撤销函数,这样父goroutine就获得了子goroutine的撤销权。</p>
<p>&nbsp;</p>
<p>原则:</p>
<p>1. 不要把context放到一个结构体中,应该作为第一个参数显式地传入函数</p>
<p>2. 即使方法允许,也不要传入一个nil的context,如果不确定需要什么context的时候,传入一个context.TODO</p>
<p>3. 使用context的Value相关方法应该传递和请求相关的元数据,不要用它来传递一些可选参数</p>
<p>4. 同样的context可以传递到多个goroutine中,Context在多个goroutine中是安全的</p>
<p>5. 在子context传入goroutine中后,应该在子goroutine中对该子context的Done channel进行监控,一旦该channel被关闭,应立即终止对当前请求的处理,并释放资源。</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/juanmaofeifei/p/14439957.html
頁: [1]
查看完整版本: go context详解