[TOC]
什么是 context 本质上 Go 语言是基于 context 来实现和搭建了各类 goroutine 控制的,并且与 select-case 联合,就可以实现进行上下文的截止时间、信号控制、信息传递等跨 goroutine 的操作,是 Go 语言协程的重中之重。
**在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。 context 的树形结构 对应着的就是 Goroutine 的树形结构 **
context 通过构建链表式的结构来实现多层级的 cancel 传递,而 timeout 的逻辑也是通过传递一个 “context deadline exceeded” 的 cancel 错误来实现所有的 信号的统一。
context 本质 我们在基本特性中介绍了不少 context 的方法,其基本大同小异。看上去似乎不难,接下来我们看看其底层的基本原理和设计。
context 相关函数的标准返回如下:
1 func WithXXXX (parent Context, xxx xxx) (Context, CancelFunc)
其返回值分别是 Context
和 CancelFunc
,接下来我们将进行分析这两者的作用。
接口 Context 接口: 1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
Deadline:获取当前 context 的截止时间。
Done:获取一个只读的 channel,类型为结构体。可用于识别当前 channel 是否已经被关闭 ,其原因可能是到期,也可能是被取消了。
Err:获取当前 context 被关闭的原因 。
Value:获取当前 context 对应所存储的上下文信息。
Canceler 接口: 1 2 3 4 type canceler interface { cancel(removeFromParent bool , err error ) Done() <-chan struct {} }
cancel:调用当前 context 的取消方法。
Done:与前面一致,可用于识别当前 channel 是否已经被关闭。
基础结构
在标准库 context 的设计上,一共提供了四类 context 类型来实现上述接口。分别是 emptyCtx
、cancelCtx
、timerCtx
以及 valueCtx
。
emptyCtx 在日常使用中,常常使用到的 context.Background
方法,又或是 context.TODO
方法。
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
其本质上都是基于 emptyCtx
类型的基本封装。而 emptyCtx
类型本质上是实现了 Context 接口:
实际上 emptyCtx
类型的 context 的实现非常简单,因为他是空 context 的定义,因此没有 deadline,更没有 timeout,可以认为就是一个基础空白 context 模板。
cancelCtx 在调用 context.WithCancel
方法时,我们会涉及到 cancelCtx
类型,其主要特性是取消事件。源码如下:
1 2 3 4 5 6 7 8 9 func WithCancel (parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func () { c.cancel(true , Canceled) } } func newCancelCtx (parent Context) cancelCtx { return cancelCtx{Context: parent} }
其中的 newCancelCtx
方法将会生成出一个可以取消的新 context,如果该 context 执行取消,与其相关联的子 context 以及对应的 goroutine 也会收到取消信息。
例子 首先 main goroutine 创建并传递了一个新的 context 给 goroutine b,此时 goroutine b 的 context 是 main goroutine context 的子集:
传递过程中,goroutine b 再将其 context 一个个传递给了 goroutine c、d、e。最后在运行时 goroutine b 调用了 cancel
方法。使得该 context 以及其对应的子集均接受到取消信号,对应的 goroutine 也进行了响应。
demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func stopWithContext () { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) go func (ctx context.Context) { for { select { case data := <-ctx.Done(): fmt.Println("监控退出,停止了..." , data, ctx.Err()) return default : fmt.Println("goroutine监控中..." ) time.Sleep(200 * time.Millisecond) } } }(ctx) time.Sleep(1 * time.Second) cancel() time.Sleep(1 * time.Second) }
源代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 type cancelCtx struct { Context mu sync.Mutex done chan struct {} children map [canceler]struct {} err error } func (c *cancelCtx) Value(key interface {}) interface {} { if key == &cancelCtxKey { return c } return c.Context.Value(key) } 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 } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err } 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 } c.err = err if c.done == nil { c.done = closedchan } else { close (c.done) } for child := range c.children { child.cancel(false , err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
timerCtx 在调用 context.WithTimeout
方法时,我们会涉及到 timerCtx
类型,其主要特性是 Timeout 和 Deadline 事件,源码如下:
1 2 3 4 5 6 7 8 9 10 11 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) { ... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } }
你可以发现 timerCtx
类型是基于 cancelCtx
类型的。我们再进一步看看 timerCtx
结构体:
1 2 3 4 5 6 7 8 9 type timerCtx struct { cancelCtx timer *time.Timer deadline time.Time }
源代码 cancel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (c *timerCtx) Deadline() (deadline time.Time, ok bool ) { return c.deadline, true } func (c *timerCtx) cancel(removeFromParent bool , err error ) { c.cancelCtx.cancel(false , err) if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() }
cancel 先会调用 cancelCtx
类型的取消事件。若存在父级节点,则移除当前 context 子节点,最后停止定时器并进行定时器重置。而 Deadline 或 Timeout 的行为则由 timerCtx
的 WithDeadline
方法实现:
timeout 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func WithDeadline (parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { return WithCancel(parent) } dur := time.Until(d) if dur <= 0 { c.cancel(true , DeadlineExceeded) return c, func () { c.cancel(false , Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func () { c.cancel(true , DeadlineExceeded) }) } return c, func () { c.cancel(true , Canceled) } }
valueCtx 在调用 context.WithValue
方法时,我们会涉及到 valueCtx
类型,其主要特性是涉及上下文信息传递,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func WithValue (parent Context, key, val interface {}) Context { ... if !reflectlite.TypeOf(key).Comparable() { panic ("key is not comparable" ) } return &valueCtx{parent, key, val} } func (c *valueCtx) Value(key interface {}) interface {} { if c.key == key { return c.val } return c.Context.Value(key) }
context 取消事件 在我们针对 context 的各类延伸类型和源码进行了分析后。我们进一步提出一个疑问点,context 是如何实现跨 goroutine 的取消事件并传播开来的,是如何实现的 ?
这个问题的答案就在于 WithCancel
和 WithDeadline
都会涉及到 propagateCancel
方法,其作用是构建父子级的上下文的关联关系,若出现取消事件时,就会进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func propagateCancel (parent Context, child canceler) { done := parent.Done() if done == nil { return } select { case <-done: child.cancel(false , parent.Err()) return default : } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { child.cancel(false , p.err) } else { if p.children == nil { p.children = make (map [canceler]struct {}) } p.children[child] = struct {}{} } p.mu.Unlock() } else { atomic.AddInt32(&goroutines, +1 ) go func () { select { case <-parent.Done(): child.cancel(false , parent.Err()) case <-child.Done(): } }() } }
通过对 context 的取消事件和整体源码分析,可得知 cancelCtx
类型的上下文包含了其下属的所有子节点信息。 也就是其在 children
属性的 map[canceler]struct{}
存储结构上就已经支持了子级关系的查找,也就自然可以进行取消事件传播了。
而具体的取消事件的实际行为,则是在前面提到的
propagateCancel方法中,会在执行例如
cacenl` 方法时,会对父子级上下文分别进行状态判断,若满足则进行取消事件,并传播给子级同步取消。
使用 我们可以通过一个代码片段了解 context.Context
是如何对信号进行同步的。在这段代码中,我们创建了一个过期时间为 1s 的上下文,并向上下文传入 handle
函数,该方法会使用 500ms 的时间处理传入的请求:
因为过期时间大于处理时间,所以我们有足够的时间处理该请求
如果我们将处理请求时间增加至 1500ms,整个程序都会因为上下文的过期而被中止,:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { ctx, cancel := context.WithTimeout(context.Background(), 1 *time.Second) defer cancel() go handle(ctx, 500 *time.Millisecond) select { case <-ctx.Done(): fmt.Println("main" , ctx.Err()) } } func handle (ctx context.Context, duration time.Duration) { select { case <-ctx.Done(): fmt.Println("handle" , ctx.Err()) case <-time.After(duration): fmt.Println("process request with" , duration) } }
总结 作为 Go 语言的核心功能之一,其实标准库 context 非常的短小精悍,使用的都是基本的数据结构和理念。既满足了跨 goroutine 的调控控制,像是并发、超时控制等。
同时也满足了上下文的信息传递。在工程应用中,例如像是链路ID、公共参数、鉴权校验等,都会使用到 context 作为媒介。
目前官方对于 context 的建议是作为方法的首参数传入,虽有些麻烦,但也有人选择将其作为结构体中的一个属性传入。但这也会带来一些心智负担,需要识别是否重新 new 一个。
也有人提出希望 Go2 取消掉 context,换成另外一种方法,但总体而言目前未见到正式的提案,这是我们都需要再思考的。
参考 一文吃透 Go 语言解密之上下文 context
上下文 Context