golang context 2022-04-20

本文对golang context的源码进行解读,go version is go1.16.4 linux/amd64

context

我们可能会创建多个 Goroutine 来协同处理一个请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求的特定数据、取消信号以及处理请求的截止日期(request data, cancel signal and deadline)

即,【goroutine with context】

两个interface: context.Context & context.canceler

  • context.Context
// 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() (deadline time.Time, ok bool) // 返回 context.Context 被取消的时间,也就是完成工作的截止时间
    // 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<- Value) error {
    //      for {
    //          v, err := DoSomething(ctx)
    //          if err != nil {
    //              return err
    //          }
    //          select {
    //          case <-ctx.Done():
    //              return ctx.Err()
    //          case out <- v:
    //          }
    //      }
    //  }
    //
    // See https://blog.golang.org/pipelines for more examples of how to use
    // a Done channel for cancellation.
    Done() <-chan struct{} // 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
    Err() error // 
    Value(key interface{}) interface{}
}
  1. Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止时间;

  2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;context取消->Channel关闭->监听的goroutine读Channel读出零值->得知context取消->goroutine退出;(从已关闭的 channel 接收数据,返回已缓冲数据或零值)

    Done() 返回的 channel本质上就是一个二值的开关:当这个 channel 被关闭时,说明 context 被取消了。注意,返回的是一个只读的channel。 我们知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出:

    • 读不出值,context没取消
    • 读出值,context被取消

    所有相关goroutine监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到

  3. Err— 返回context.Context结束的原因,它只会在Done 方法对应的 Channel 关闭时返回非空的值;

    1. 如果 context.Context 被取消,会返回 Canceled 错误;
    2. 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  4. Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

  • context.canceler
type canceler interface {
    cancel(removeFromParent bool, err error)  // removeFromParent 表示是否主动断绝与父Context的关系
    Done() <-chan struct{}
}

对于实现了Context接口的数据类型,再实现一个cancel(removeFromParent bool, err error)方法,就变成了可取消的Context

为什么canceler要实现cancel和Done两个方法呢?

  • 首先,和Context类似,多个goroutine都可以监听canceler.Done()返回的只读channel,当canceler发生取消操作时,每个监听者都将收到消息

  • cancel() 方法的功能就是关闭自己的 channel:close(c.done);然后loop取消它的所有子Context(调用child.cancel(false, err),并且主动断绝和子Context的关系c.children = nil);最后让父Context在children中删除自己

    父子Context切断联系,本质上就是从p.children中删除child

源码中有两个类型实现了 canceler 接口:*cancelCtx*timerCtx

2个主要的cancel结构体

// 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 c 的Context记录了c的parent,children 记录了c的child。这样一来,cancelCtx的上下游联系都被完整记录了
其实,从数据结构的视角看,这就是在最通用的tree结点的基础上,加入了与父结点的关系,从而完全打通了与上下的交互;同时,也有点双向链表的意思

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
       // 将parent作为父节点context生成一个新的子节点
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    if done == nil {
        return // parent is never canceled 父Context永远不会取消,直接return
    }

    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:
    }
        //  下面这个if很重要
        // 【尝试把父Context转成*cancelCtx p,如果可以转,就把child加到父Context(as a cancelCtx)的children中】
        // 【这样只要父cancelCtx调用cancel,其所有child都会跟着被cancel】
    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{})
            }
                       // 把child加到父Context的children中
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
                // 【如果父Context无法转换成*cancelCtx,那么只能直接开个goroutine监听parent.Done(),当parent取消时,紧接着child.cancel】
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

context.WithCancel能从当前的 context.Context 中衍生出一个新的子上下文并返回用于取消该子上下文的cancel函数
context.WithCancel 将通过func propagateCancel(parent Context, child canceler)构建父子context的关联,父context取消时,子context也将被取消。通过一个个这样的父子链关系传递,可以取消整个context树。下面看下具体cancel()方法的实现:取消自己,取消children,根据需要断绝自己和父Context的关系

// 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 // var closedchan = make(chan struct{}), closedchan is a reusable closed channel.
    } else {
        close(c.done)
    }
    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()

    if removeFromParent {
        removeChild(c.Context, c)  // c.Context refers to c's parent
    }
}
  • context.timerCtx

    type timerCtx struct {
      cancelCtx
      timer *time.Timer // Under cancelCtx.mu
      deadline time.Time
    }
    

    context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能
    *context.timerCtx的cancel调用了*cancelCtx.cancel(removeFromParent bool, err error)(其实也没那么简单,见下)

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()
}

这里有一点需要思考:
为什么调用c.cancelCtx.cancel时removeFromParent直接指定为false呢?直接写成下面的样子不可以吗?

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(removeFromParent, err)
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

这其实是context的父子关系决定的。需要搞清楚的是,c.cancelCtx.Context是c的父context,而非c.cancelCtx的父context,这是WithDeadline创建timerCtx的时候确定的:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    ...
    // 创建了一个timerCtx并把它与parent建立父子关系
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)  // not propagateCancel(parent, c.cancelCtx)
    ...
}

因此,直接c.cancelCtx.cancel(removeFromParent, err)并不能解除c与c.cancelCtx.Context的关系,而是取消了c.cancelCtx与c.cancelCtx.Context的关系

context.WithDeadline 在创建 context.timerCtx 的过程中还判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号

Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到

参考https://www.cnblogs.com/qcrao-2018/p/11007503.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容