⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Aug.8 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
目录
有特定需要的内容直接跳转到相关章节查看即可。
Section 1. 为什么需要 Context 标准库?:阐述了 Google 开发 Context 标准库的起因;
Section 2. context 的工作模型:阐述了 Context 要实现的工作流程模型;
Section 3. context 关键数据结构:分析了 package context 中的关键数据结构;
3.1 context 接口:介绍了 context interface,所有的 context 结构都对它做了直接/间接实现;
3.2 空上下文:emptyCtx:介绍了用于充当根节点的 emptyCtx;
3.3 传递值的上下文:valueCtx:介绍了可用于传递值的 valueCtx;
3.4 有取消功能的上下文:cancelCtx:介绍了可用于手动取消的 cancelCtx;
3.5 定时取消功能的上下文:timerCtx:介绍了可用于定时取消的 timerCtx;
3.5.1 :WithDeadline():按时间点取消;
3.5.2 :WithTimeout():按运行时长取消;
Section 4. context 的使用:通过例子阐述了 Context 的使用方法;
1. 为什么需要 Context 标准库?
在 Go 的服务器中,每个 Request 都在其自己的 goroutine 中处理,这些 Request 处理程序通常会启动额外的 goroutine 来访问后端,例如数据库和 RPC 服务。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如用户的身份、授权令牌和 Request 的截止日期。当 Request 被取消或超时时,处理该请求的所有 goroutine 都应该快速退出,以便系统可以回收它们正在使用的任何资源。
就取消 Request 这一点来说,在 Golang 并发编程: 并发的退出 中,我们知道我们可以通过使用 close(chan) 的方法来广播消息,来实现由于超时、取消操作或者一些异常情况而进行抢占操作或者中断等的后续操作。我们在那篇文章中介绍了最基本的广播消息的办法。
但是,最基本的广播消息的方法是不实用的。考虑这样一种场景:main gorotinue 起了三个处理子任务的 worker gorotinue,这三个 worker gorotinue 又会起再下一层的 gorotinue。我们需要保证:当我们取消一个 gorotinue 的时候,下面所有由该 gorotinue 创建的 gorotinue 以及它们衍生出来的再往下一级的 gorotinue 都应该被取消。倘若采用我们最基本的广播消息的方法来实现 gorotinue 的取消,那么整个程序将会变得异常庞大复杂,并且毫无章法,可读性很低,无法通过阅读代码来理解 gorotinue 之间的层级关系。
为了解决这个问题,Google 开发了一个 context 包 (context: 上下文之意),可以轻松地将 Request 的信息、取消信号和截止日期跨 API 边界传递给处理请求所涉及的所有 goroutine。该包作为上下文公开可用。
在本文中,我们将描述该包的使用方法和实现细节。
2. context 的工作模型
在深入代码之前,我们首先应该理解 context 的工作流程模型以及背后的动机。
![](./pic/context.png)
如上图所示,为了封装在 gorotinue 之间进行传递的上下文信息,我们使用了好几种 Struct (i.e. emptyCtx, cancelCtx, timerCtx, valueCtx),这几种 Struct 中封装了 parent-gorotinue 与 sub-gorotinue 之间需要同步的信息 (e.g. 取消信号、超时时间等),分别满足不同的上下文信息传输需求,我们暂且把它们统称为 Context Struct。一个 parent-gorotinue 通常会创建一个 Context Struct,然后将这个 Context Struct 作为参数传递给 sub-gorotinue。这就是 Context Struct 的一般的用法。
我们回忆一下我们想要实现的基础目标是什么:当一个 gorotinue 被取消时,我们想要它本身停止工作,并且由该 gorotinue 创建的 gorotinue 也停止工作。因此,我们更希望与将我们的 Context Struct 形成一种树状结构,因为它们之间明显存在一种派生的关系。举个例子,如下图所示,当 gorotinue 1-1 想要通过 Context Struct 1 告诉 Layer 2 的 gorotinue 停止它们的工作时,gorotinue 2-1 只需要停止它自己的工作就可以了,而 gorotinue 2-2 需要利用 Context Struct 2 来告诉 Layer 3 中由它创造的 gorotinue 停止它们的工作。这样一来,我们会发现,实际上合理的做法是 "打通" Context Struct 2 & 3 和 Context Struct 1,也就是说让 Context Struct 2 & 3 能够传递 Context Struct 1 的关闭信号,这样当我们在 gorotinue 1-1 发送取消信号的时候,就能广播到所有应该被取消的 gorotinue 中去。
![](./pic/context_tree.png)
在 package context 中,结构体 cancelCtx 和 timerCtx 被设计用于上述的递归取消问题。
除了传递关闭信号,我们有时候还需要在 gorotinue 之前传递上下文信息。在 package context 中,也提供了结构体 valueCtx 用于在 gorotinue 之间传输数据。
在理解了 context 的工作流程模型后以及背后的动机之后,我们下面将要展开对具体代码的分析,看看在 golang 中是如何实现上面所描述的工作流程的。
3. context 关键数据结构
本章中我们将阐述我们在上一章所描述的几种 Context Struct (i.e. emptyCtx, cancelCtx, timerCtx, valueCtx),这几种 Context Struct 分别有着不同的特性,满足了不同的上下文需求。但是 Context Struct 都实现了最基本的 context 接口,因此我们首先来看这个接口。
3.1 context 接口
1 | type Context interface { |
在 context 接口的成员中,第一个是 Done 方法,它返回了一个 chan,作为子协程的取消信号:当这个 chan 关闭时,子协程应该放弃它们的工作并返回。我们在后面的程序中将看到,子协程在程序中将会一直根据 Done() 方法的返回值去判断是否有取消信号产生。这一点与我们在 Golang 并发编程: 并发的退出 中看到的基于广播消息实现协程取消的方法是很相似的。
然后是 Err 方法,它用于返回一个 error 用于描述为什么上面所述的 chan 被关闭了。
而 Deadline 方法 返回了一个 time.Time,表示当前 Context 应该结束的时间。如果没有指定应该结束的时间,则该函数的 ok 返回值为 false。
最后, Value 方法 用于使能 Context 来携带和正在处理的 Request 有关的数据 (Request-scope Data),用于父子协程之间的数据交互。注意!这些数据必须是可以安全地被多个子 gorotinue 并发的使用的数据。
3.2 空上下文:emptyCtx
现在我们来看第一种 Context Struct: emptyCtx。我们在 context 的工作模型 中说到,整个程序的 Context Struct 实际上最终形成是一棵树。可以预想到,这棵树的根结点肯定是在 main gorotinue 中。我们通常会在 main gorotinue 中通过 context.Background() 获取一个 Context Struct 实例,作为 Context Struct 树的根结点:
1
2
3
4// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
Background 实际上返回的是一个 emptyCtx 结构,emptyCtx 结构实际上是 int 类型并且完成了对 context 接口的实现。emptyCtx 的相关源码如下:
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
40type 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"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
可以看到,context.Background() 实际上返回的是内部的 emptyCtx 实例 background。并且,由于 emptyCtx 的 Done() 方法返回值一直是 nil,所以实际上它是一个永远不会被取消的 Context Struct。同理由于它的 Value 方法返回一直是 nil,因此它是一个不带值的 Context Struct。这么设计的原因是它被我们当做根 Context Struct 使用,它不能被取消,也不必要去携带任何的值。
3.3 传递值的上下文:valueCtx
valueCtx 顾名思义就是提供一个带有数据存储功能的 Context Struct。与 emptyCtx 是一个 int 类型不同,valueCtx 是一个结构体,其定义如下所示:
1
2
3
4
5
6
7
8
9
10
11type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
在 valueCtx 中,首先有一个成员 Context,用于继承 parent-Context Struct,然后还有一个 key-value 对。我们在上面的代码可以看到 valueCtx "重写" 了 Value 方法,这里说 "重写" 是因为 valueCtx 的成员 Context 已经有了 Value 方法。我们可以看见 valueCtx 的 Value 方法传入了一个参数 key,它回去找到这个 key 对应的 value,如果自己存储的不是这个 key,它就会去它上游的 Context 中找,直到拿到一个结果 (可能最终找到了,也可能返回一个 nil)。
可以看到,Value() 的获取是采用 链式获取 的方法。如果当前 Context 中找不到,则从父 Context 中获取。如果我们希望一个 Context 多放几条数据时,可以保存一个 map 数据到 valueCtx 中。这里不建议多次构造 valueCtx 来存放数据。毕竟取数据的成本也是比较高的。
valueCtx 仅重写了 Value 方法,其它的 context 接口的方法仍然继承于 parent-Context Struct,因此其自身仍然是不可被取消的 Context Struct,因为它没有实现 Done() 方法。
我们可以使用下面的函数来基于一个 Context 实例化一个 valueCtx:
1
2
3
4
5
6
7
8
9
10
11
12func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
3.4 有取消功能的上下文:cancelCtx
cancelCtx 顾名思义就是提供带有取消功能的 Context Struct,作为 gorotinue 之间的传输内容,它使得 parent-gorotinue 能够手动终止它下面的 sub-gorotinue 以及 sub-gorotinue 创建的 sub-sub-gorotinue (如此递归)。cancelCtx 同样是一个结构体,其定义如下所示:
1
2
3
4
5
6
7
8type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done 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
}
在 cancelCtx 中同样包含了一个成员 Context,其存储的是 parent-Context Struct 的一份拷贝,这一点我们在后面的初始化函数中就会看到。
cancelCtx 的关键成员 done 表示一个 channel,用来表示传递关闭信号。注意到我们上面介绍的 emptyCtx 和 valueCtx 各自都没有实现 done channel:emptyCtx 的 Done() 函数直接返回了 nil;valueCtx 没有实现 Done(),它的 Done() 函数最终都依赖于上游继承的 Context Struct。
我们下面会看到,cancelCtx 实际上实现了 canceler 接口:实现了 cancelCtx.Done() 函数用于返回这个 done chan;实现了 cancelCtx.cancel() 函数用于关闭这个 done chan。
值得注意的是,虽然 cancelCtx 的成员 Context 存储的是一份 parent 的拷贝,但是当我们调用 parent 的 Done() 函数的时候,如下图所示,如果 parent 有 done chan,那操作的就是 parent 的 done chan;如果 parent 没有 done chan,那操作的就是祖先 Context 的 done chan;如果祖先 Context 也没有 done chan,那么归根到底返回的就是 context.Background().Done(),返回的是 nil。
![](./pic/done_chain.png)
回到 cancelCtx,它的第五个成员 err 很好理解,用于存储错误信息表示任务结束的原因。它的第四个成员 children 表示一个 map,存储了当前 context 节点下的子节点。注意到 map 的 key 使用的是 canceler,canceler 是一个接口,其定义如下所示:
1
2
3
4
5
6// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
在 canceler 接口中,cancel() 用于发送取消信号,Done() 用于返回 done chan。也就是说,实现了 canceler 接口的 Context Struct 即带有取消功能。
cancelCtx 本身实现了 canceler 接口。cancelCtx 的这个 map 成员存储的实际上是下游实现了 canceler 接口的 Context Struct,当我们在取消一个实现了 canceler 接口的 Context Struct 时,同时还需要把下游的实现了 canceler 接口的 Context Struct 也一起取消,所以我们需要对这些实现了 canceler 接口的下游 Context Struct 进行存储。回过头来,我们来看 cancelCtx 对 canceler 接口的实现细节,首先是 Done():
1
2
3
4
5
6
7
8
9func (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
}
Done() 函数我们在上面讲过,本质就是返回一个 chan,通过关闭这个 chan 可以用来广播 cancel 消息。因此我们看到 cancelCtx 的 Done() 方法就是返回了 cancelCtx 结构体中的 done chan:如果还没有初始化,就创建一个并返回;如果已经初始化,就直接返回。
再来看 cancel():
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// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
// 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) {
// STEP 1: cancel 自己本身
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 设置取消原因
c.err = err
// 设置一个关闭的 channel 或者将 done channel 关闭,用以发送关闭信号
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// STEP 2: 将子节点 context 依次取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// STEP 3: 将当前 context 节点从父节点上移除 (if enabled)
if removeFromParent {
removeChild(c.Context, c)
}
}
cancel() 函数用于关闭 done chan,我们会看到它有两步组成:取消 cancelCtx 自己和 取消 child-cancelCtx。并且如果传入参数 removeFromParent 为 true,cancelCtx 还会把自己从上游的 parent-cancelCtx 的 map[canceler]struct{} 中删除。
理解了 cancelCtx 本身能实现的功能之后,我们现在关注一下如何新建一个 cancelCtx 并且完成相关的初始化操作。我们可以调用函数 context.WithCancel(parent Context) 来派生出一个 cancelCtx,相关代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13func 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) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
在上面的代码中,context.WithCancel(parent Context) 使用内部函数 newCancelCtx 实例化了一个 cancelCtx 结构,并且调用了内部函数 propagateCancel(),propagateCancel() 函数的作用是实现当上游的 cancelCtx 被取消时,我们当前创建的这个 cancelCtx 也会被取消。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// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 返回上游最近的 cancelCtx
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 {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
Line 3-6 的代码判断了上游是否存在 done chan,如果不存在的话,说明不存在上游永远不会被取消,则直接返回。
Line 8-14 的代码判断了上游的 done chan 是否已经被关闭,如果已经被关闭,则直接把自己也关闭。
然后我们关注 Line 17 的 parentCancelCtx(parent)。当它第二个返回值 ok 为 true 时,它的第一个返回值就是上游最近的 cancelCtx,我们直接往这个上游的 cancelCtx 的 map[canceler]struct{} 里添加我们正在初始化的 cancelCtx,如下图所示的 Case 1,这一点在上面的 Line 18 - 26 进行了实现。这样一来当上游的 cancelCtx 被取消了之后,我们这个 cancelCtx 也会被取消,这一点我们在讲解 cancelCtx 的 cancel() 函数时就已经讲解过了。
![](./pic/cancel_done.png)
当它第二个返回值 ok 为 false 时,这里指的是:parent 调用 Done() 返回的 done chan 不是由上游的 cancelCtx 返回的,而是由一个用户自定义的实现了新的 done chan 的 Context Struct 返回的,如上图所示的 Case 2。这样一来,我们的做法就不应该是把我们正在初始化的结构体放到上游的 cancelCtx 中,并且上游有可能根本就没有 cancelCtx,而是应该起一个 gorotinue 去监听这个用户自定义的 Context Struct 的 done chan,只要它关了,我们正在初始化的这个 cancelCtx 也得关。这部分在上面代码的 Line 30 - 37 进行了实现。这算是一个很小的细节了。
具体的 parentCancelCtx(parent) 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
p.mu.Lock()
ok = p.done == done
p.mu.Unlock()
if !ok {
return nil, false
}
return p, true
}
回过头来,总结一下:我们可以使用 context.WithCancel(parent Context) 来获得一个 cancelCtx,并且把这个绑到上游去。只要上游的遇见的第一个 done chan 被关闭了之后,这个 cancelCtx 也会被自动关闭。我们注意到 context.WithCancel(parent Context) (ctx Context, cancel CancelFunc) 的第二个返回值是一个函数,这个函数可以供上游 gorotinue 手动调用来将自己和底下的 cancelCtx 给取消掉,这个函数内部其实就是调用了当前 cancelCtx 的 cancel 函数。
3.5 定时取消功能的上下文:timerCtx
在理解了带有手动取消功能的 cancelCtx 后,我们来看看能够实现定时取消的 timerCtx。它的定义如下所示:
1
2
3
4
5
6
7type timerCtx struct {
cancelCtx
// 计时器
timer *time.Timer
// 截止时间
deadline time.Time
}
timerCtx 内部包含了一个 cancelCtx,也就是说它是复用了 cancelCtx 结构的取消功能。
我们下面将通过 WithDeadline() 和 WithTimeout() 这两个构造函数来理解 timerCtx 的工作原理:
(1) WithDeadline()
WithDeadline(parent Context, d time.Time) 用于创建一个在约定时间点会被取消的 timerCtx,其定义如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 建立新建context与可取消context祖先节点的取消关联关系
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 {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
其逻辑如下所示:
- 如果父节点 parent 有过期时间并且过期时间早于给定时间 d,那么新建的子节点 context 无需设置过期时间,使用 WithCancel 创建一个可取消的 context 即可;
- 否则,就要利用 parent 和过期时间 d 创建一个定时取消的 timerCtx,并建立新建 context 与可取消 context 祖先节点的取消关联关系,接下来判断当前时间距离过期时间 d 的时长 dur;
- 如果 dur 小于 0,即当前已经过了过期时间,则直接取消新建的 timerCtx,原因为 DeadlineExceeded;
- 否则,为新建的 timerCtx 设置定时器,一旦到达过期时间即取消当前 timerCtx
(2) WithTimeout()
与 WithDeadline 类似,WithTimeout 也是创建一个定时取消的 context,只不过 WithDeadline 是接收一个过期时间点,而 WithTimeout 接收一个相对当前时间的过期时长 timeout:
1
2
3func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
4. context 的使用
在下面的例子中,我们让子协程监听主协程传入的 ctx,一旦ctx.Done() 返回空channel,子线程即可取消执行任务。
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
33func main() {
messages := make(chan int, 10)
// producer
for i := 0; i < 10; i++ {
messages <- i
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// consumer
go func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-ctx.Done():
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}(ctx)
defer close(messages)
defer cancel()
select {
case <-ctx.Done():
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
}
上面这个例子还无法展现 context 的传递取消信息的强大优势。我们在 package net/http 一文中理清楚了 package net/http 的大部分细节,细心的同学可能注意到在实现 http server 时候,源码中就用到了 context, 下面我们补充一下这部分的实现。
在创建完 socket 后,Server.ListenAndServe 会调用 Server.Serve(l net.Listener) 对接收到的请求进行处理,其代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func (srv *Server) Serve(l net.Listener) error {
...
var tempDelay time.Duration // how long to sleep on accept failure
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
...
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
首先 Server 在开启服务时会创建一个 valueCtx (i.e. 上面代码的 Line 7),存储了 Server 的相关信息,之后每建立一条连接就会开启一个协程,并携带此 valueCtx (i.e. 上面代码的 Line 16)。
下面我们观察一下进入各个连接后发生了什么事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
...
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
...
for {
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
...
}
}
建立连接之后会基于传入的 context 创建一个 valueCtx 用于存储本地地址信息 (i.e. 上面代码的 Line 3),之后在此基础上又创建了一个 cancelCtx (i.e. 上面代码的 Line 6),然后开始从当前连接中读取网络请求,每当读取到一个请求则会将该 cancelCtx 传入,用以传递取消信号 (i.e. 上面代码的 Line 13)。一旦连接断开,即可发送取消信号,取消所有进行中的网络请求。
下面我们看看读取请求过程的代码:
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
32func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
...
req, err := readRequest(c.bufr, keepHostHeader)
...
ctx, cancelCtx := context.WithCancel(ctx)
req.ctx = ctx
...
w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),
// We populate these ahead of time so we're not
// reading from req.Header after their Handler starts
// and maybe mutates it (Issue 14940)
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
...
return w, nil
}
读取到请求之后,会再次基于传入的 context 创建新的 cancelCtx (i.e. 上面代码的 Line 9),并设置到当前请求对象 req 上 (i.e. 上面代码的 Line 10),同时生成的 response 对象中 cancelCtx 保存了当前 context 取消方法 (i.e. 上面代码的 Line 16)。
这样处理的目的主要有以下几点:
- 一旦请求超时,即可中断当前请求
- 在处理构建 response 过程中如果发生错误,可直接调用 response 对象的 cancelCtx 方法结束当前请求
- 在处理构建 response 完成之后,调用 response 对象的 cancelCtx 方法结束当前请求
在整个 server 处理流程中,使用了一条 context 链贯穿 Server、Connection、Request,不仅将上游的信息共享给下游任务,同时实现了上游可发送取消信号取消所有下游任务,而下游任务自行取消不会影响上游任务。
![](./pic/context_http.png)
附录:参考源
- Sameer Ajmani, golang.org, Go Concurrency Patterns: Context
- Go 语言中文网,Go 语言坑爹的 WithCancel
- 知乎, 深入理解Golang之context