context 底层原理是什么
context 包的代码并不长,context.go 文件总共不到 500 行,其中还有很多大段的注释,代码 可能也就 200 行左右的样子,是一个非常值得研究的代码库。
先看一张整体的图,如图 7-3 所示,
其具体功能见表 7-1。
上面这张表展示了 context 的 所有函数、接口、结构体。 整体类图如图 7-4 所示。
# 接口
# Context
直接看源码:
// src/context/context.go
type Context interface {
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
2
3
4
5
6
7
8
9
10
11
1)Context 是一个接口,定义了 4 个方法,它们都是幂等的,意味着连续多次调用同一个方法,得到的结果都是相同的。
2)Deadline () 返回 context 的截止时间,通过此时间,用户就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置 一个 I/O 操作的超时时间。
3)Done () 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时, 说明 context 被取消了。注意,这是一个只读的 channel。并且,读一个关闭的 channel 会读出相应类型的零值,而源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。
4)Err () 返回一个错误,表示 channel 被关闭的原因。例如被取消,还是超时。
5)Value () 获取之前设置的 key 对应的 value。
# Canceler
再来看另外一个接口:
// src/context/context.go
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
2
3
4
5
6
对于实现了上面定义的两个方法的 context,就表明是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意它们是加了 * 号的,即这两个结构体的指针类型实现了 canceler 接口。
canceler 接口设计成这样的原因:
1)“取消” 操作应该是建议性,而非强制性。
Caller 不应该去关心、干涉 callee 的情况, 决定如何以及何时 return 是 callee 的责任。 Caller 只需发送 “取消” 信号,callee 根据收到的信号来做进一步的决策,因此 Context 接口并没 有定义 cancel 方法。
2)“取消” 操作应该可传递
“取消” 某个函数时,和它相关联的其他函数也应该 “取消”。因此,Done () 方法返回一个只读的 channel,所有相关函数监听同一个 channel。一旦 channel 关闭,通过 channel 的 “广播机制”,所有监听者都能收到 “取消” 信号。
# 结构体
源码中定义了 Context 接口,给出了一个 emptyCtx 的实现:
// src/context/context.go
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
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
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
每个函数都实现的异常简单,要么是直接返回,要么是返回 nil。所以,这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。
它被包装成:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
2
3
4
通过下面两个导出的函数(首字母大写)对外公开:
func Background() Context {
return background
}
func TODO() Context {
return todo
}
2
3
4
5
6
7
变量 background 通常用在顶层函数中,作为所有 context 的根节点。
变量 todo 通常用在不知道传递什么 context 的情形。例如,调用一个需要传递 context 参数的函数,但手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在进行重构的过程中,给一些函数添加了一个 context 参数,但不知道要传什么,就用 todo “占个位子”,但最终要换成其他有意义的 context。
再来看一个重要的 context,即 cancelCtx:
// src/context/context.go
type cancelCtx struct {
Context
// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
2
3
4
5
6
7
8
9
10
这是一个可以取消的 context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。
先来看 Done () 方法的实现:
// src/context/context.go
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
}
2
3
4
5
6
7
8
9
10
11
函数中,c.done 是 “懒汉式” 创建,只有调用了 Done () 方法的时候才会被创建。再次强调, 函数返回的是一个只读的 channel,而且源码中没有地方向这个 channel 里面写数据。所以,直接读这个 channel 的协程会被 block。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。
Err () 和 String () 方法比较简单,不详细解释。接下来,重点关注 cancel () 方法的实现:
// src/context/context.go
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
// 相当于没有调 Done() 方法,所以不需要真正关闭 c.done,而且 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)
}
}
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
总体来看,cancel () 方法的功能就是关闭 c.done;递归地取消它的所有子节点;从父节点中删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。一般地,goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 分支被选中。
再来看创建一个可取消的 context 的方法:
// src/context/context.go
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) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
WithCancel 是一个暴露给用户的方法,传入一个父 context(这通常是一个 background,作为根节点),返回新创建的 context,新 context 的 done channel 是新建的。
当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点 的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。
注意传给 WithCancel 方法的参数,前者是 true,也就是说取消的时候,需要将自己从父节点里删除。第二个参数则是一个固定的取消错误类型:
var Canceled = errors.New("context canceled")