目录

go上下文库 context 笔记

context

参考

上下文(context)是在go中是非常常见的概念之一,平时做api开发时也经常见到。但一直这么迷迷糊糊地用也不是办法,还是尝试了解一下。

上下文这个概念与并发有关。在go中,控制并发有channel,waitgroup,context,以及传统的锁控制等。其中channel用户协程间通信,waitgroup用于控制协程数量,而context的主要应用场景则为子孙协程层层嵌套。

阅读相关内容时,总有一种割裂感,好像讲的和我用的不太一样,实际上是因为go原生的context只是一个接口,而web框架通常对其做了封装实现(如gin.context)。使用成品库时常常有种知其然而不知所以然的感觉,所以还是了解一下原生的代码实现。

首先是Context本身的接口定义:

type Context interface {

   Deadline() (deadline time.Time, ok bool)

   Done() <-chan struct{}

   Err() error

   Value(key any) any
}
  • Deadline() 有两个返回值,前者为取消时间,后者为是否设置deadline。
  • Done() 用于指示上下文是否取消。
  • Err() 返回上下文关闭的原因,未关闭时返回nil。
  • Value() 返回对应的键值,key不存在或不支持该方法时返回nil。

emptyCtx 就是空的上下文,可以通过 context.Background()context.TODO() 来进行创建。

在创建上下文后,可以通过with系列方法创建子上下文。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

如下是相关的源码实现:

var (
  background = new(emptyCtx)
  todo       = new(emptyCtx)
)

func Background() Context {
  return background
}

func TODO() Context {
  return todo
}

context.Background()context.TODO() 方法返回的值是一样的,只是在使用和语义上稍有不同。

  • context.Background() 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO() 应该只在不确定应该使用哪种上下文时使用。
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 any) any {
   return nil
}

emptyCtx 的底层类型使用int而非空结构体,是因为 emptyCtx 的实例必须要有不同的内存地址。

emptyCtx 没法被取消,没有 deadline,也不能取值,实现的方法都是返回零值。

type valueCtx struct {
   Context
   key, val any
}

func (c *valueCtx) Value(key any) any {
   if c.key == key {
      return c.val
   }
   return value(c.Context, key)
}

valueCtx类型只是添加了一个键值对,以及实现了Value()方法。并且对Value()方法的实现也很简单,当前找不到就去父上下文找。

以我个人常用的gin.Context来说,传值时基本不会用上这个Value方法,而是内部实现一个map[any]any,通过Set()/Get()方法操作,避免了不断衍生子上下文的层层嵌套。Value方法则主要用于兼容请求传来的http.Request.Context()

cancelCtx实现了canceler接口,如下:

type canceler interface {
    // removeFromParent 表示是否从父上下文中删除自身
    // err 表示取消的原因
  cancel(removeFromParent bool, err error)
    // Done 返回一个管道,用于通知取消的原因
  Done() <-chan struct{}
}

当调用WithCancel()创建cancelCtx时,首先通过propagateCancel()方法尝试将自身添加进父级的children中实现级联取消,然后闭包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     atomic.Value          // of 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
    cause    error                 // set to non-nil by the first cancel call
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    // 尝试将自身添加进父级的children中
    c.propagateCancel(parent, c)
    return c
}

type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := withCancel(parent)
    return c, func() { c.cancel(true, Canceled, nil) }
}

如下是propagateCancel()方法的具体实现,逻辑大致如下:

  • 如果parent.Done() == nil,即父上下文不会取消时,直接返回。
  • 如果父上下文已经取消时,取消子上下文并返回。
  • 将子上下文加入父上下文的children列表;
  • 新建协程监听父子上下文的取消信号:
    • 如果父上下文取消,取消子上下文;
    • 如果子上下文取消,退出协程。
// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
    c.Context = parent

    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err(), Cause(parent))
        return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
        // parent is a *cancelCtx, or derives from one.
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err, p.cause)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
        return
    }

    if a, ok := parent.(afterFuncer); ok {
        // parent implements an AfterFunc method.
        c.mu.Lock()
        stop := a.AfterFunc(func() {
            child.cancel(false, parent.Err(), Cause(parent))
        })
        c.Context = stopCtx{
            Context: parent,
            stop:    stop,
        }
        c.mu.Unlock()
        return
    }

    goroutines.Add(1)
    go func() {
        select {
        case <-parent.Done():
            child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()
}

最后是cancelCtx实现的cancel()方法,遍历子上下文逐个取消,然后将自己从父上下文的children中删除。

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

timerCtxcancelCtx 的封装,增加了超时机制,可以通过WithDeadline()WithTimeout()创建。前者指定具体时间,何时超时;后者指定时间间隔,过多久超时。此外,后者也是前者的封装。

// 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
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

让我们将重点放到WithDeadline()上,逻辑大致如下:

  • 如果子上下文的到期时间晚于父上下文的到期时间,将直接继承父上下文。
  • 如果已经到期,直接取消子上下文并返回。
  • 通过time.AfterFunc()设置定时器,到期时取消子上下文。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    return WithDeadlineCause(parent, d, nil)
}

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
        return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, cause)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}