【golang】context包

191 阅读8分钟

前言

大家应该会经常看到这样的代码:很多方法第一个参数都是context,而且还一路往下透传......

本文章的目标就是学习了解context包的作用、应用场景、最佳实践、原理&源码。

简介-是什么、有什么用

context包是go 1.7开始引入到go标准库的,是专门用来简化对于处理单个请求的多个goroutine之间与 请求域的 数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用

作用:

  1. 在goroutine之间传递、共享上下文信息
  1. 在goroutine之间传递取消信号,用于控制goroutine执行,比如:取消goroutine执行、超时取消goroutine执行
  1. 在一个goroutine的方法调用链路上传递、共享上下文信息(这个作用类似Java的ThreadLocal)

目前我们常用的一些库都支持context,例如gindatabase/sql等库都是支持context的,这样更方便我们做并发控制了,只要在请求处理入口创建一个context上下文,不断透传下去即可。

demo

WithValue

使用context包的func WithValue(parent Context, key, val any) Context,会返回valueCtx实例

作用:设置键值对,共享数据

const LogIDKey = "LogID"
const CallerKey = "caller"

func main() {
   ctx := context.Background()
   ctx = context.WithValue(ctx, LogIDKey, "123456")
   go test1(ctx)
   go test2(ctx)
   for {
   }
}

func test1(ctx context.Context) {
   fmt.Println("test1 run... logId:", ctx.Value(LogIDKey))
   ctx = context.WithValue(ctx, CallerKey, "test1")
   test111(ctx)
}

func test111(ctx context.Context) {
   fmt.Printf("test111 run... caller: %s, logId:%s\n", ctx.Value(CallerKey), ctx.Value(LogIDKey))
}

func test2(ctx context.Context) {
   fmt.Println("test2 run... logId:", ctx.Value(LogIDKey))
   ctx = context.WithValue(ctx, CallerKey, "test2")
   test222(ctx)
}

func test222(ctx context.Context) {
   fmt.Printf("test222 run... caller: %s, logId:%s\n", ctx.Value(CallerKey), ctx.Value(LogIDKey))
}

运行结果:

test1 run... logId: 123456
test111 run... caller: test1, logId:123456
test2 run... logId: 123456
test222 run... caller: test2, logId:123456

WithCancel

使用context包的func WithCancel(parent Context) (ctx Context, cancel CancelFunc)方法,会返回cancelCtx实例和CancelFunc函数。

作用:通过调用返回的CancelFunc,可以取消所有子goroutine执行。取消子goroutine执行的原理:实际就是利用channel发送取消信号,子goroutine必须监听channel。

const callerKey = "Caller"

var logger = log.Default()

func main() {
   ctx := context.Background()
   ctx, cancelFunc := context.WithCancel(ctx)
   go method1(ctx)
   go method2(ctx)
   <-time.After(3 * time.Second)
   cancelFunc()

   for {

   }
}

func method1(ctx context.Context) {
   ctx = context.WithValue(ctx, callerKey, "method1")
   go method3(ctx)
   for {
      logger.Println("method1 run...")
      select {
      case <-ctx.Done():
         logger.Println("method1 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

func method2(ctx context.Context) {
   for {
      logger.Println("method2 run...")
      select {
      case <-ctx.Done():
         logger.Println("method2 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

func method3(ctx context.Context) {
   for {
      logger.Println("method3 run...caller:", ctx.Value(callerKey))
      select {
      case <-ctx.Done():
         logger.Println("method3 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

运行结果:

3秒后,method1method2method3的goroutine都退出了。

WithTimeout

使用context的func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)方法,返回timerCtx实例和CancelFunc函数。

作用:

通过调用返回的CancelFunc函数主动取消或者超时自动取消所有子goroutine执行,实际也是利用channel发送取消信号,子goroutine必须监听channel。

const callerKey = "Caller"

var logger = log.Default()

func main() {
   ctx := context.Background()
   ctx, _ = context.WithTimeout(ctx, 3*time.Second)
   go method1(ctx)
   go method2(ctx)

   for {

   }
}

func method1(ctx context.Context) {
   ctx = context.WithValue(ctx, callerKey, "method1")
   go method3(ctx)
   for {
      logger.Println("method1 run...")
      select {
      case <-ctx.Done():
         logger.Println("method1 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

func method2(ctx context.Context) {
   for {
      logger.Println("method2 run...")
      select {
      case <-ctx.Done():
         logger.Println("method2 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

func method3(ctx context.Context) {
   for {
      logger.Println("method3 run...caller:", ctx.Value(callerKey))
      select {
      case <-ctx.Done():
         logger.Println("method3 end...")
         return
      default:
         <-time.After(time.Second)
      }
   }
}

运行结果:

3秒后,method1method2method3的goroutine都退出了。

Context接口及实现

Context接口:

建议看看源码,有注释最佳实践,比如:Done()Value(key any) any的最佳实践

type Context interface {
   // 返回一个Deadline,如果没设置Deadline,则ok返回false
   Deadline() (deadline time.Time, ok bool)

   // 返回一个channel。
   // 若当前上下文「不能取消」,会返回nil
   // 若当前上下文「已取消或超时」,则返回的是【已关闭】的channel
   // 最佳实践:通常在select语句中使用Done
   Done() <-chan struct{}

   // 若Done未关闭,则返回nil。若Done已关闭(取消或超时导致关闭),则返回non-nil
   Err() error

   // 返回上下文中key对应的value。最佳实践:key通常是全局变量,且是unexported,且是可比较类型
   Value(key any) any
}

go标准库自带的实现有:

  1. emptyCtx
// 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
  1. valueCtx
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
   Context
   key, val any
}
  1. cancelCtx

注意:当前ctx被取消,会取消所有实现了canceler的子ctx

// 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
}
  1. timerCtx
// 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
}

Context的特性

  • 树形结构
  • 并发安全

树形结构

Context整体是一个树形结构(多叉树),树的不同节点可能是不同Context的实现,每次调用WithCancelWithValueWithTimeoutWithDeadline实际就是创建一个子节点。

从源码中可以看到valueCtxcancelCtxtimeCtx都需要一个父ctx,而emptyCtx是作为根节点使用的。


重点:

  1. emptyCtx的作用就是作为根节点
  1. 一个go程序只有两个context根节点,即只有两个emptyCtx实例:backgroundtodo

源码如下:

整个程序中只有backgroundtodo两个emptyCtx实例,不能再创建emptyCtx

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

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

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
   return todo
}
  1. 兄弟节点之间是独立的,要想一个ctx节点不影响另一个ctx节点,则这两个ctx节点必须在不同子树上
  1. 如果我们想要新起的goroutine不受当前ctx控制,可以基于emptyCtx派生出新的ctx,然后传给新起的goroutine

示例:

func main() {
   ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
   go method1(ctx)

   for {
   }
}

func method1(ctx context.Context) {
   log.Default().Println("method1 start")

   go method2(ctx)

   // 基于emptyCtx派生出新ctx,而不是基于当前ctx派生出新ctx
   ctx2, _ := context.WithTimeout(context.Background(), 5*time.Second)
   go method3(ctx2)

   for {
      select {
      case <-ctx.Done():
         log.Default().Println("method1 exit")
         return
      case <-time.After(time.Second):
         log.Default().Println("method1 run")
      }
   }
}

func method2(ctx context.Context) {
   log.Default().Println("method2 start")
   for {
      select {
      case <-ctx.Done():
         log.Default().Println("method2 exit")
         return
      case <-time.After(time.Second):
         log.Default().Println("method2 run")
      }
   }
}

func method3(ctx context.Context) {
   log.Default().Println("method3 start")
   for {
      select {
      case <-ctx.Done():
         log.Default().Println("method3 exit")
         return
      case <-time.After(time.Second):
         log.Default().Println("method3 run")
      }
   }
}

运行结果:可以看到method1和method2 goroutine同时退出了,而method3 goroutine还能继续运行。

源码

类图

emptyCtx

// 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 any) any {
   return nil
}

emptyCtx实现了Context接口的所有方法,都是返回空

valueCtx

valueCtx自己只实现了Context接口的Value方法,其他方法都继承自父ctx

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
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)
}

func value(c Context, key any) any {
   for {
      switch ctx := c.(type) {
      case *valueCtx:
         if key == ctx.key {
            return ctx.val
         }
         c = ctx.Context
      case *cancelCtx:
         if key == &cancelCtxKey {
            return c
         }
         c = ctx.Context
      case *timerCtx:
         if key == &cancelCtxKey {
            return &ctx.cancelCtx
         }
         c = ctx.Context
      case *emptyCtx:
         return nil
      default:
         // 自定义实现的ctx
         return c.Value(key)
      }
   }
}

// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

// cancelCtx对Value方法的实现,timerCtx直接继承了cancelCtx对Value方法的实现
func (c *cancelCtx) Value(key any) any {
   if key == &cancelCtxKey {
      return c
   }
   return value(c.Context, key)
}
// emptyCtx对Value方法的实现
func (*emptyCtx) Value(key any) any {
   return nil
}

总结:

  1. Value方法获取key对应的值,若当前context节点没有该key,会递归往父节点找,直至根节点。

cancelCtx

cancelCtx自己实现了Context接口的ValueDoneErr方法,其他方法都继承自父ctx

func (c *cancelCtx) Value(key any) any {
   // 通过cancelCtxKey,可获取cancelCtx本身。
   if key == &cancelCtxKey {
      return c
   }
   return value(c.Context, key)
}

func (c *cancelCtx) Done() <-chan struct{} {
   d := c.done.Load()
   if d != nil {
      return d.(chan struct{})
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   d = c.done.Load()
   if d == nil {
      d = make(chan struct{})
      c.done.Store(d)
   }
   return d.(chan struct{})
}

func (c *cancelCtx) Err() error {
   c.mu.Lock()
   err := c.err
   c.mu.Unlock()
   return err
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   c := newCancelCtx(parent)
   propagateCancel(parent, &c)
   return &c, func() { c.cancel(true, Canceled) }
}

// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()

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

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
// 这个接口相当于定义了一个「可取消」的context类型
type canceler interface {
   cancel(removeFromParent bool, err error)
   Done() <-chan struct{}
}

// cancelCtx对Done方法的实现,懒初始化Done channel
func (c *cancelCtx) Done() <-chan struct{} {
   d := c.done.Load()
   if d != nil {
      return d.(chan struct{})
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   d = c.done.Load()
   if d == nil {
      d = make(chan struct{})
      c.done.Store(d)
   }
   return d.(chan struct{})
}
  • CancelFunc实际是调用ctx的cancel方法

详解propagateCancel方法:

核心:

  1. 若父ctx已取消,则取消child
  1. 绑定父子关系。取消父ctx时,就能找到所有子ctx并取消
func propagateCancel(parent Context, child canceler) {
   done := parent.Done()
   if done == nil {
      // 表示parent ctx是不可取消的,且父ctx继承的所有祖先节点也都不能取消
      return
   }

   select {
   case <-done:
      // 若parent ctx已经取消了,则立马取消child
      child.cancel(false, parent.Err())
      return
   default:
   }

   // 从parent ctx开始往根节点找cancelCtx
   if p, ok := parentCancelCtx(parent); ok {
      p.mu.Lock()
      if p.err != nil {
         // err非nil,表示parent ctx已经取消了,则立马取消child
         child.cancel(false, p.err)
      } else {
         if p.children == nil {
            p.children = make(map[canceler]struct{})
         }
         // 绑定父子关系
         p.children[child] = struct{}{}
      }
      p.mu.Unlock()
   } else {
      // 处理自定义的ctx
      atomic.AddInt32(&goroutines, +1)
      go func() {
         select {
         case <-parent.Done():
            // 取消child
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

看看cancelCtx实现的cancel方法:

核心:

  1. 关闭Done channel,此后Done方法返回的是已关闭的channel。即Done方法返回的是已关闭channel,则表示当前ctx已取消
  1. 赋值err,此后Err方法返回非nil。即Err方法返回的是非nil,则表示当前ctx已取消
  1. 递归关闭取消child
// 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) {
   // 取消后,err不能是nil
   if err == nil {
      panic("context: internal error: missing cancel error")
   }
   c.mu.Lock()
   if c.err != nil {
      // err非nil,表示当前ctx已取消,直接返回
      c.mu.Unlock()
      return
   }
   // 赋值非nil的err
   c.err = err
   d, _ := c.done.Load().(chan struct{})
   if d == nil {
      // 表示Done channel还未初始化过,直接存储closedchan
      c.done.Store(closedchan)
   } else {
      // 关闭Done channel
      close(d)
   }
   
   // 取消当前ctx的所有child,每个child又会往下递归取消自己的child
   for child := range c.children {
      // NOTE: acquiring the child's lock while holding parent's lock.
      child.cancel(false, err)
   }
   // 删除所有child
   c.children = nil
   c.mu.Unlock()

   // 把当前ctx从父ctx那删除
   if removeFromParent {
      removeChild(c.Context, c)
   }
}

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
   p, ok := parentCancelCtx(parent)
   if !ok {
      return
   }
   p.mu.Lock()
   if p.children != nil {
      delete(p.children, child)
   }
   p.mu.Unlock()
}

总结:

  1. 核心几个方法:

    1. propagateCancel:绑定父子关系,子是canceler类型
    2. parentCancelCtx:获取最近的父cancelCtx
    3. cancel:取消当前ctx,并取消所有子ctx
  1. Done方法返回nil,表示「当前ctx + 所在的继承链上的所有祖先ctx」都不可取消
  1. 不会向Done channel发送数据,而是仅关闭Done channel,关闭后然后消费方消费就不再阻塞了

timerCtx

timerCtx自己只实现了Context接口的Deadline方法,其他方法都继承自父ctx

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

func WithDeadline(parent Context, d time.Time) (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)
   }
   
   // 创建timerCtx
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   
   propagateCancel(parent, c)
   
   dur := time.Until(d)
   if dur <= 0 {
      c.cancel(true, DeadlineExceeded) // deadline has already passed
      return c, func() { c.cancel(false, Canceled) }
   }
   
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {
      // 等于nil表示当前ctx未取消,则创建一个计时器:等待一定时间后再取消
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

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

// timerCtx对cancel方法的实现
func (c *timerCtx) cancel(removeFromParent bool, err error) {
   // 委托cancelCtx,所以底层是cancelCtx
   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 {
      // 停止timer
      c.timer.Stop()
      c.timer = nil
   }
   c.mu.Unlock()
}

总结:

  1. timerCtx的底层原理是cancelCtx,比cancelCtx多了deadlinetimer,重新实现了cancel方法:1. 通过cancelCtx实现取消 2.停止timer计时器

谈谈用ctx取消goroutine执行的原理

关键:
一个协程无法被外部协程取消,只能自己取消自己

所以,利用ctx取消goroutine执行其实就是利用ctx发送一个取消信号,其他goroutine会监听到这个信号,至于要不要取消执行则由自己决定

最佳实践

  1. 不建议使用context传递关键参数,关键参数应该显式声明,不应该隐式处理,context中最好是携带token、traceId这类值。

自定义不可取消的ctx

ctx的Done()方法返回nil表示该ctx是不能cancel的,可以使用这种ctx摆脱父ctx的取消控制

如下,自定义不可取消的ctx ValueOnlyCtx

// ValueOnlyCtx 只能获取值,不能被取消
type ValueOnlyCtx struct {
   // 父ctx
   context.Context
}

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

func (*ValueOnlyCtx) Done() <-chan struct{} {
   return nil
}

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

测试:

const LogID = "LogID"

var globalCtx = context.Background()

func init() {
   globalCtx = context.WithValue(globalCtx, LogID, "test666")
   globalCtx, _ = context.WithTimeout(globalCtx, 2*time.Second)
}

func TestT1(t *testing.T) {
   // 此时异步任务【会】受ctx控制被取消
   go asyncTask(globalCtx)

   <-time.After(5 * time.Second)
}

func TestT2(t *testing.T) {
   // 使用不可取消的ctx,使异步任务摆脱父ctx的取消控制
   ctx := &ValueOnlyCtx{Context: globalCtx}
   // 此时异步任务【不会】受ctx控制被取消
   go asyncTask(ctx)

   <-time.After(5 * time.Second)
}

func asyncTask(ctx context.Context) {
   lodID := ctx.Value(LogID)
   count := 0
   // 任务耗时,正常情况下异步任务要执行这么长时间才完成
   taskCostTime := time.After(3 * time.Second)
   for {
      <-time.After(500 * time.Millisecond)
      count++
      logger.Printf("LogID:%s asyncTask run %d", lodID, count)

      select {
      case <-taskCostTime:
         logger.Println("asyncTask completed")
         return
      case <-ctx.Done():
         logger.Println("asyncTask canceled")
         return
      default:
      }
   }
}

测试结果:
TestT1测试用例运行结果如下:

=== RUN   TestT1
2023/03/22 00:23:59 LogID:test666 asyncTask run 1
2023/03/22 00:24:00 LogID:test666 asyncTask run 2
2023/03/22 00:24:00 LogID:test666 asyncTask run 3
2023/03/22 00:24:01 LogID:test666 asyncTask run 4
2023/03/22 00:24:01 asyncTask canceled
--- PASS: TestT1 (5.00s)
PASS

可以看到异步任务被取消

TestT2测试用例运行结果:

=== RUN   TestT2
2023/03/22 00:25:36 LogID:test666 asyncTask run 1
2023/03/22 00:25:37 LogID:test666 asyncTask run 2
2023/03/22 00:25:37 LogID:test666 asyncTask run 3
2023/03/22 00:25:38 LogID:test666 asyncTask run 4
2023/03/22 00:25:38 LogID:test666 asyncTask run 5
2023/03/22 00:25:39 LogID:test666 asyncTask run 6
2023/03/22 00:25:39 asyncTask completed
--- PASS: TestT2 (5.00s)
PASS

可以看到异步任务正常完成,所以可以使用「自定义的不可取消的ctx」使异步任务摆脱父ctx的取消控制。

并发安全

多个goroutine能并发安全的读写ctx中的值,如何实现并发安全的?一个ctx能被多个goroutine并发安全的使用,如何实现并发安全的?

答案:Context是一个不可变对象,每次修改Context都是创建一个新结构体。

总结

  1. 了解了context的作用、应用、特性
  1. 看源码,学习思想、学习go语言编程
  1. goroutine并发编程三大神器:channel、context以及sync包

好文推荐

mp.weixin.qq.com/s/HzODjvg42…
go.dev/blog/pipeli…