Go知识库Go常用库

理解 Go context

2018-07-15  本文已影响65人  小小小超子

深入理解 Go Context

什么是 Context

Context 的最常见但也是最不准确的翻译是 ‘上下文’(因为程序里通常只需要上文),其实译为 ‘语境’ 更为合适,意思是当前说话的环境。最直观的作用是提供一些必要的信息:

...

唐僧:“悟空~”

question:唐僧的“悟空” 表达了怎样的心理?

answer:。。。去你的

Context 的概念本身比较宽泛,从系统角度说,线程/进程 的切换时,需要先保存当前寄存器和栈指针,然后载入下一个 进程/线程 需要的寄存器和栈。寄存器和栈就是进程/线程的 Context。

在不同编程语言中,也有不同体现:

如 c 语言的 errno (摘自某呼):

注意过 errno 这个全局变量的朋友会发现,这个全局变量其实有可能不是一个真正的变量.它返回了一个本地线程的存储空间.它实际上是每个线程有一份.这里,其实 C 语言运行时已经悄悄变成了多份,而对应当前线程的实例用本地线程保存,它就是一个 context

又例如 Javascript 在浏览器中运行就有浏览器作为环境提供 window 对象,而在 node.js 环境下面运行就没有 window 对象。

照此看来,Context 好像就是一个 ‘全局变量’,那为什么不直接声明全局变量,非要用 Context 这个生涩的概念呢?

再结合轮子哥说的:

每一段程序都有很多外部变量。只有像 Add 这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫 Context

那么我们可以认为,Context 就是把一些信息打包聚合到一起,形成一个模块交互的语境,各个模块像传递包裹一样取用它,而不是通过全局变量来访问它。

Go 语言里的 Context

Context 的使用

Go 语言的 Context 在携带信息的基础上,增加了非常实用的功能,设计也非常简洁巧妙。标准库提供了可携带 value 的 Context可取消的 Context可超时的 Context

携带 value 的 Context

前面提到 Context 最基本的作用是携带语境中的一些信息,比如一些参数。但是问题来了,所有参数都要放到 Context 吗?哪些应该、哪些不应该?如果一个函数如下:

func a(key string, value interface, id int){
    ...
}

如果把参数全都放到 context:

func a(ctx context.Context){
    ...
}

前者我们可以一目了然的从函数签名中获取或猜出一些关于这个函数的大概信息,而后者只看函数签名获得不了什么信息,需要仔细的从代码里读。很明显,前者可读性更高。一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。

使用 Context 携带参数会让接口定义更加模糊。那么什么样的信息应该放到 Context 里呢?官方注释如下:

Use context values only for request-scoped data that transits processes and API boundaries, not for passing optional parameters to functions.

也就是说,应该保存 Request 范畴的值:

。。。好像这句话说了和没说差不多?在处理请求的时候,难道不是所有的信息都来自 Request ?

其实通常来说, Context.Value 应该是 告知性质 的东西,而不是 控制性质 的东西。

哪些不是控制性质的?
显然是控制性质的:

关于 可携带 value 的 Context,还有一个值得注意的地方是:Context 本身是不可变的(immutable),让一个 Context 携带新的参数并不是一个 “setter” 来修改 Context 值,而是通过“包含”的形式,生成一个新的 Context 包含原有 Context,形成链式结构。在下面实现的时候继续讨论。

可取消 和 可超时的 Context

为什么要取消(超时的本质也是取消,只不过通过计时器触发取消操作)?

这和 Go 语言的 goroutine 有关。当你在 c 程序中 fork 一个新的进程,你会得到一个 PID,你可以通过这个 PID 向它发送信号来停止它的运行。

可是当你启动一个 goroutine 时,你并不会得到一个这个‘线程‘的 ID,那么要如何才能关掉它呢?答案就是 可取消的 Context

官方示例:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // Even though ctx will be expired, it is good practice to call its
    // cancelation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

几点问题

当你搜索关于 Go Context 的博客的时候,通常你会看到一些规则:

  • 不要将 Context 放入结构体, Context 应该作为第一个参数传入,命名为 ctx.
  • 即使函数允许, 也不要传入nil 的 Context. 如果不知道用哪种 Context,可以使用 context.TODO().
  • 使用 context 的 Value 相关方法,只应该用于在程序和接口中传递和请求相关数据,不能用它来传递一些可选的参数
  • 相同的 Context 可以传递给在不同的 goroutine; Context 是并发安全的.

可是有几点问题:

Go 的 Context 实现

在标准库里,Context 是一个接口:

type Context interface {
   Deadline() (deadline time.Time, ok bool)

   Done() <-chan struct{}

   Err() error
   
   Value(key interface{}) interface{}
}

而我们常用的 context.Background() 返回的是一个最基本的全局 context:background,是一个什么功能也没有的 emptyCtx:

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

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

其他所有的 Context 都应该衍生自这两个基本的 ctx,生成新的 context 的方式是找一个 ‘父亲’ ,然后复制它,再结合 value 或者 timer 生成新的 context。

withValue

func WithValue(parent Context, key, val interface{}) Context {
   if key == nil {
      panic("nil key")
   }
   if !reflect.TypeOf(key).Comparable() {
      panic("key is not comparable")
   }
   return &valueCtx{parent, key, val} // 返回的是一个指针
}

type valueCtx struct {
   Context // 注意这里使用匿名域
   key, val interface{}
}

每次添加 value 不是改变了context ,而是在原有的 context 基础上重新生成一个,形成了一条链。获取 value 的时候是逆序的:

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

先看最后一个节点的键值对,如果不是,那么沿着链往上查找:

value_ctx_chain.png

withCancel

由于可超时的 Context 是基于可取消的 Context 实现的,所以这里只讨论 cancelCtx:


type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

type cancelCtx struct {
   Context

   mu       sync.Mutex            // 由于多个线程都可能执行 ctx.Cancel(),要加锁
   done     chan struct{}         // created lazily, closed by first cancel call
   children map[canceler]struct{} // 由于需要在父节点取消时取消其所有字节点,所以记录其所有可取消子节点
   err      error                 // set to non-nil by the first cancel call
}

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

生成一个新的 可取消 Context 的时候,需要传入一个父 Context 节点,并且通过父节点找到祖先节点里面最近的一个可取消的 Context 节点,然后把自己记录在那个祖先节点的 children 里面,这样在祖先被 cancel 的时候,新的这个 Context 也会被取消。不过为什么是祖先节点而不是父节点呢?因为可能有如下情况(图中箭头方向代表生长方向):

cancelCtx.png

其父节点可能不是可取消的,所以没法记录 children,所以不难理解代码了:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   c := newCancelCtx(parent) // 生成一个新的可取消节点
   propagateCancel(parent, &c) // 找到可取消祖先并记录自己到祖先的 children
   return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent}
}

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
   if parent.Done() == nil {
       return // 这里尤其注意,parent.Done() 返回 nil,表示整个链上都没有可取消/可超时的 context。因为新的 Context 在包含父节点的时候,都是采用匿名字段,也就是说,如果新的 Context 本身没有某个函数,但是它的匿名字段上有那个函数,那么该函数是可以直接被新的 Context 调用的。如此就可以一直追溯到 background 节点,而正好这个根节点是有 Done() 这个函数,并且返回 nil。另外,不可能出现中间一个可取消 context 调用 Done() 返回 nil,看实现便知。
   }
   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{})
         }
         p.children[child] = struct{}{}
      }
      p.mu.Unlock()
   } else {  // 没想通的是这里,什么情况会走到这步呢?
      go func() {
         select {
         case <-parent.Done():
            child.cancel(false, parent.Err())
         case <-child.Done():
         }
      }()
   }
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   for { // 沿着父节点往上找,直到找到一个 可取消的/可超时的 祖先节点
      switch c := parent.(type) {
      case *cancelCtx:
         return c, true
      case *timerCtx:
         return &c.cancelCtx, true
      case *valueCtx:
         parent = c.Context
      default:
         return nil, false
      }
   }
}

知道如何注册 cancelCtx,那么具体 cancel 的实现也很简单了,就是先取消自己,然后根据 children 递归遍历并取消所有可取消子节点。代码就不贴了,有兴趣自己看一遍完整源码比较合适。

最后再放一张图,更清楚的理解它们的关系:

ctx_relation.png

reference

上一篇下一篇

猜你喜欢

热点阅读