前言
context 包中的代码虽然只有 600 多行,但已经成为了并发控制、超时控制的标准做法,可以说是真正的短小而精悍,是十分值得研读的 Go 源码之一。本文首先从整体的视角解析了 context 的主要接口和函数,分析了其中重要结构的实现关系,以及存储所用的数据结构;随后针对 context 接口不同的实现源码进行了详细的解析,可以帮助大家更有效理解不同 Context 的实现原理。(关于具体使用,我们下期再聊)
注:本文依据 go 版本 1.20.7 源码进行讲解 源码地址:src/context/context.go
1.context 简介
1.1 context 是什么?
context 在 Go 1.7 版本被引入标准库,包 context 定义了 Context 类型,它携带跨 API 边界和进程之间的截止日期、取消信号和其他请求范围的值。context 在 Golang 中的作用是为了提供一种在函数之间传递请求作用域数据、控制请求执行时间、处理并发任务等功能的机制,以便更加有效地管理和控制请求的执行。特别是在处理并发请求和微服务架构中,context 的作用更加凸显。
1.2 为什么需要 context
举一个常见的例子:大量 http 请求不断地访问服务器,每一个请求都会开多个协程去处理这个请求的业务逻辑,例如:获取用户信息(基本信息、权限信息、其他额外的信息等),如果下游业务处理逻辑发生异常,长时间未返回处理结果,上游长时间的等待将会导致严重的超时,业务服务可能会因为协程没有释放导致协程泄漏,极端情况还会引发服务器雪崩。因此,协程之间能够进行事件通知并且能控制协程的生命周期非常重要,这时候就应该使用 context 包了,context 主要就是用来在多个协程中设置截止日期、同步信号,传递请求相关值。每一次 context 都会从顶层一层一层的传递到下面一层的协程中,当上面的 context 取消的时候,下面所有的 context 也会随之取消。
因此,在 Golang 中引入 context 的主要原因是为了更好地管理并发请求 和控制请求的执行。在处理并发请求的情况下,可能会涉及到多个并发任务、超时控制、任务取消等需求,而 context 提供了一种标准化的方式来处理这些问题。具体来说,引入 context 主要有以下几个原因:
- 控制请求执行时间 :在处理 HTTP 请求或者其他并发任务时,可以使用 context 来设置截止时间或者超时时间,确保请求不会无限期地执行。这对于避免资源的过度占用和及时释放资源非常重要。
- 取消操作 :context 提供了一种统一的机制来取消请求的执行。在某些情况下,可能需要取消已经启动的任务,或者在某些条件下终止任务的执行,这时可以使用 context 来实现。
- 传递请求作用域的数据 :context 可以用于在多个函数之间传递请求的元数据,例如请求 ID、用户身份信息、语言环境等。这在处理微服务架构或者处理 HTTP 请求时非常有用。
- 跨 API 边界传递请求作用域数据 :当请求需要经过多个 API 边界时,可以使用 context 来传递请求作用域的数据,确保在整个请求处理链路中能够获取到必要的信息和控制。
- 并发任务管理:通过 context 可以控制多个并发任务的执行,比如通过一个 context 对象去取消多个并发任务的执行,或者等待多个任务中的一个完成。
总的来说,context 的使用场景涵盖了处理并发请求、控制请求执行时间、取消操作、请求作用域数据传递等多个方面,适用于处理并发请求、微服务架构中的请求处理等多种场景。
2.context 源码整体概览
研读源码,最好先从整体视角分析一下包中函数、接口、类(Go 中多为结构体)之间的结构关系。下图画出了 context 包中的主要函数、接口和类的关系,context 包主要由两个接口(Context 、canceler) 以及四个结构体 (emptyCtx、valueCtx、cancelCtx、timerCtx) 组成,其中类的实现关系如图所示。emptyCtx 一般作为 root 节点使用;valueCtx、cancelCtx、timerCtx 通过不同的函数依据祖先 Context 派生出来,结构体中通过嵌入的方式拥有对祖先 Context 的回溯机制(被称为回溯链),后续会进行详细介绍。
下边这张表列举了 context 包中所有重要的变量、函数、接口以及结构体,起到总览全局和查找复习的作用,等阅读完全文,可以再来回顾一下。
类型 | 名称 | 作用 |
---|---|---|
Canceled | error 变量 | errors.New("context canceled") |
DeadlineExceeded | error 变量 | deadlineExceededError{};deadlineExceededError 结构体实现了 error 接口;return "context deadline exceeded" |
background | Context 实例变量 | new(emptyCtx) |
todo | Context 实例变量 | new(emptyCtx) |
cancelCtxKey | int 变量 | &cancelCtxKey 被用来寻找第一个父级 cancelCtx |
closedchan | channel 变量 | 已关闭的channel,在init 中直接调用了 close(closedchan) |
CancelCauseFunc | 函数类型 | func(cause error) 带原因的取消函数类型定义 |
CancelFunc | 函数类型 | type CancelFunc func() 取消函数类型定义 |
Context | 接口类型 | 定义了 Context 接口(四个方法:Deadline、Done、Err、Value) |
canceler | 接口类型 | 定义了取消接口(两个方法:cancel、Done) |
stringer | 接口类型 | 该接口只有一个方法:String() string |
deadlineExceededError | 结构体类型 | 实现了 error 接口 |
emptyCtx | int 类型 | 实现了 Context,是个空 Context |
cancelCtx | 结构体类型 | 组合了 Context 接口,用于存储 parent ctx ;并实现了 canceler 接口,可以被取消 ;存储了 map[canceler]struct{} 用于和 children 关联 |
timerCtx | 结构体类型 | 组合了 cancelCtx 结构体,拥有了 cancelCtx 的功能 ;携带了 timer 和 deadline 参数,能够定时取消 |
valueCtx | 结构体类型 | 组合了 Context 接口,用于存储 parent ctx ;key, val interface{} 用于存储 key:value(可能为空) |
Background | 函数 | 返回一个空 context background,常作为根 context |
TODO | 函数 | 返回一个空 context todo |
Cause | 函数 | 返回一个 error 表示被取消原因 |
WithCancel | 函数 | 基于 parent context 创建可取消的 context 和 取消函数 |
WithCancelCause | 函数 | 基于 parent context 创建可取消的 context 和 带取消原因的取消函数 |
withCancel | 函数 | 根据祖先 ctx 创建一个可取消的 cancelCtx,当祖先 ctx 取消时安排孩子取消 |
newCancelCtx | 函数 | 根据祖先 ctx 创建一个可取消的 cancelCtx |
propagateCancel | 函数 | 当祖先 ctx 取消时安排孩子取消;parent context 不可取消 done = nil 不用建立关联;parent context 已取消 -- child.cancel 取消 child;parent context 为 *cancelCtx 时,p.children[child] 利用 cancelCtx 中的 map 结构建立父子关联,方便在祖先取消时,一并取消孩子 ctx;parent context 不为 *cancelCtx 时,单独起携程监测祖先 done 时,取消孩子 ctx |
parentCancelCtx | 函数 | 返回 parent 的第一个祖先 cancelCtx 节点 |
removeChild | 函数 | 用于解除父子关联 delete(p.children, child) map中删除 |
WithDeadline | 函数 | 创建一个 *timerCtx 包含 deadline ;内部存在一个定时器 timer *time.Timer;时间到的时候执行 cancel 方法 |
WithTimeout | 函数 | 创建一个 *timerCtx 包含 deadline;WithDeadline(parent, time.Now().Add(timeout)) |
WithValue | 函数 | &valueCtx{parent, key, val} ;创建一个存储 k-v 的 context |
init | 函数 | close(closedchan) 初始化关闭 closechan |
value | 函数 | 聚合实现了不同结构体的 value 方法 |
2.1 接口
2.1.1 Context 接口
go
type Context interface {
// 返回一个 channel,用于判断 context 是否结束
// 多次调用同一个 context done 方法会返回相同的 channel
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 当 context 结束时才会返回错误,有两种情况
// context 被主动调用 cancel 方法取消:Canceled
// context 超时取消: DeadlineExceeded
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
简单介绍一下方法的作用:
Done()
方法返回一个只读的 channel,源码中不会往里塞值,我们都知道读一个关闭的 channel 会读出相应类型的零值,因此只有 channel 被关闭,才能从该 channel 中读出值,子协程也可以根据该 channel 来判断自己是否应该结束。当 Context 被主动取消或者超时自动取消时,该 Context 所有派生 Context 的 done channel 都被 close 。所有子协程通过该字段收到 close 信号后,应该立即中断执行、释放资源然后返回。Err()
返回一个错误,表示 channel 被关闭的原因。例如是被取消(Canceled error),还是超时(DeadlineExceeded error)。Deadline()
如果本 Context 被设置了时限,则该函数返回 ok=true 和对应的到期时间点。否则,返回 ok=false 和 nil。Value()
返回绑定在该 Context 链上的给定的 Key 的值,如果没有,则返回 nil。
2.1.2 canceler 接口
go
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
canceler 接口定义了两个方法,实现了这两个方法的 Context 为可取消的 Context,比如: *cancelCtx 和 *timerCtx。
cancel()
方法为取消操作,负责关闭 Done() 返回的 channel,以及调用所有 children 的 cancel 方法;removeFromParent == true 表示和祖先断绝联系,err, cause 表明取消的错误和理由。Done()
方法返回一个只读的 channel,用于判断 context 是否结束。
2.2 实现 Context 接口的结构体
在阅读源码后会发现,Context 各种创建方法其实主要只使用到了 4 种类型的 Context 实现,也就是前文经常提到的四种实现,本小节就来简单介绍一下这 4 种类型的 Context。
2.2.1 emptyCtx
emptyCtx 本质是一个 int 类型,正如其名 emptyCtx 实现的 Context 全部返回了 nil。它通常用于创建 root Context,标准库中 context.Background()
和 context.TODO()
返回的就是这个 emptyCtx。
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 any) any {
return nil
}
2.2.2 cancelCtx
cancelCtx 是 context 包中十分重要的数据结构,*cancelCtx 是一个可被取消的 Context,*cancelCtx 实现了 canceler 接口,同时内嵌了 Context 作为匿名字段,这样就会被派生为一个 Context。
go
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
cause error // set to non-nil by the first cancel call
}
cancelCtx 结构中有一个字段 children map[canceler]struct{}
用于维护 parent canceler 与 children canceler 之间的关系,后面 WithCancel 篇章会详细讲到树结构的建立过程。
接下来看一下 *cancelCtx 的方法,首先是 Value()
方法:用于寻找第一个祖先(最近的祖先) *cancelCtx 实例。
go
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
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:
return c.Value(key)
}
}
}
cancelCtxKey
是 context 包中定义的私有变量,如果是 *cancelCtx 遇到 &cancelCtxKey
就会返回自己,否则沿着回溯链往上寻找,看看有没有 parent 是 cancelCtx,如果有就返回寻找到的第一个祖先 cancelCtx。这个其实是复用了 Value 函数的回溯逻辑,从而在 Context 树回溯链中遍历时,可以找到给定 Context 的第一个祖先 *cancelCtx 实例。使用方式:p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
。
接下来是 Done() 方法:返回一个只读 channel 用于判断是否取消。
c.done 是"懒汉式"创建,只有调用了 Done() 方法的时候才会被创建,而且只有 *cancelCtx 实现了非空 Done 函数。
go
func (c *cancelCtx) Done() <-chan struct{} {
// 原子变量获取存储的通道信息
d := c.done.Load()
if d != nil {
// 不为 nil 则直接返回
return d.(chan struct{})
}
// 并发锁 - 要执行并发写操作
c.mu.Lock()
// defer 解锁
defer c.mu.Unlock()
// 二次判断原子信息是否已经被其他协程写入
d = c.done.Load()
if d == nil {
// 没有被写入,则初始化 chan
d = make(chan struct{})
// 存入原子信息
c.done.Store(d)
}
// 返回通道 chan
return d.(chan struct{})
}
接下来 Err() 方法:
go
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
String() 方法
go
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
cancel() 方法:该方法用于关闭 c.done 并且取消其 children,该方法不对外暴露,最终以函数返回值形式暴露出去:cancel CancelFunc。
go
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
// 并发写锁
c.mu.Lock()
// err 不为 nil,表示已经被取消过,那则不用继续取消了
if c.err != nil {
// 解锁
c.mu.Unlock()
return // already canceled
}
// 设置取消原因和错误
c.err = err
c.cause = cause
// 取出 chan
d, _ := c.done.Load().(chan struct{})
// chan 未初始化
if d == nil {
// 直接存入已关闭的 chan,通知取消操作
c.done.Store(closedchan)
} else {
// 关闭 chan,通知取消
close(d)
}
// 祖先取消,孩子也得跟着取消(级联取消)
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
// 取消 child
child.cancel(false, err, cause)
}
// 断绝与所有后代的关系
c.children = nil
// 解锁
c.mu.Unlock()
// 如果想断绝与祖先的关系
if removeFromParent {
// 断绝与祖先的关系
removeChild(c.Context, c)
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
// 回溯找到第一个祖先 *cancelCtx
// 只有 *cancelCtx 中存在 children map[canceler]struct{},才有父子关系
p, ok := parentCancelCtx(parent)
// 没找到,意味着没有父子关系,不用断绝
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
// 祖先的 children map 中移除对该孩子的关系
delete(p.children, child)
}
p.mu.Unlock()
}
2.2.3 timerCtx
一个 timerCtx 携带一个定时器和一个截止时间,有了这两个配置以后就可以在特定时间进行自动取消;它嵌入了一个 *cancelCtx 来实现 Done 和 Err,它通过停止计时器然后委托给 *cancelCtx.cancel
来实现取消。
go
// 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
}
Deadline() 方法:返回截止时间
go
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
String() 方法
go
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
cancel() 方法
go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
// 委托给 cancelCtx 执行 cancel
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
// 停止计时
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
2.2.4 valueCtx
valueCtx 内部同样包含了一个 Context 接口实例,由原 Context 实例派生,valueCtx 结构中包含一对 key-value 用于存储键值对,在调用 valueCtx.Value(key interface{})
会进行递归向上查找 key 对应的 val,但是这个查找只负责查找 "直系" Context,也就是说可以无限递归查找 parent Context 是否包含这个 key,但是无法查找兄弟 Context 是否包含。
go
// 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
}
其所包含方法较为简单,如下:
go
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
3.回溯链与树构建
对 context 包有过了解的都知道,Context 是一种回溯链 + 树形结构构成,为了方便大家理解,这里画一个图。
图中虚线代表回溯链,实线代表树形结构。
回溯链: context 包中可以通过 WithCancel、WithDeadline、 WithTimeout 或 WithValue 等函数根据祖先 Context 派生出孩子 Context,新派生的 Context 嵌入了祖先 Context 实例,形成了回溯链结构(C0~C4),图中由虚线表示,value 函数就是通过该回溯链向上一层层寻找。
树形结构: cancelCtx 结构体中包含字段 children map[canceler]struct{}
,可以关联祖先和孩子节点,Context 树实质上是一颗 canceler(*cancelCtx 和 *timerCtx)树(C1、C2、C4 形成一棵树),map 中只存储了可取消节点间的父子关系,因为在级联取消的时候只需要找到子树中所有的 canceler 节点(循环 map 中的 key,cancelCtx 小节代码有使用到),对其进行取消,就可以完成对子树所有节点生命周期的掌控。
有同学会有疑问那 valueCtx 这一层如何被取消呢? 通过对 valueCtx 源码分析可以知道,valueCtx 并没有实现非空 Done 方法,其实四种 ctx 实现中只有 cancelCtx 实现了非空 Done 方法,那也就意味着调用 ctx.Done() 会直接转发到第一个祖先 cancelCtx 上,返回它的 done channel。因此当图中的 C1 节点取消时,关闭了自身的 done channel,而 C3 节点(valueCtx)中的 done channel 其实就是 C1 节点已关闭的 done channel,因此对其生命周期也进行了管控,其实就是自己的生命周期。
3.1 回溯链
回溯链是通过嵌入父级 Context 来进行构造的,主要作用有以下两点:
- value() 函数可以沿着回溯链向上查找匹配的键值对。
- 利用 value() 函数逻辑沿着回溯链查找最近的 cancelCtx 祖先,用于构造 Context 树。
3.1.1 回溯链的构建
回溯链构建与四个主要函数息息相关:WithCancel、WithDeadline、WithTimeout、WithValue, 这里详细分析一下回溯链的构建过程,其他细节(树构建)后边讨论。
WithCancel:通过 &cancelCtx{Context: parent}
构建回溯链。
go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
// 回溯链构建
c := newCancelCtx(parent)
// 树构建(后边详细介绍)
propagateCancel(parent, c)
return c
}
// 回溯链构建
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
WithDeadline:通过内嵌 cancelCtx 构建回溯链。
go
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)
}
// 构造回溯链
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 树构建(后边详细介绍)
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
// 设置的时候就已经改取消了,直接调用取消,返回取消 err,意味着已取消
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
// c.cancel 已经解除与祖先关系,这里返回的 func 就不需要再解除了
return c, func() { c.cancel(false, Canceled, nil) }
}
// 上锁
c.mu.Lock()
// 解锁
defer c.mu.Unlock()
if c.err == nil {
// 还未被取消,因为取消后 err != nil
// 设置定时器,进行取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
}
// 这里返回的 func 需要解除与祖先的关系
return c, func() { c.cancel(true, Canceled, nil) }
}
WithTimeout:复用 WithDeadline 构建回溯链。
go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithValue:通过 &valueCtx{parent, key, val} 构建回溯链。
go
func WithValue(parent Context, key, val any) 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.1.2 回溯链的作用
- value() 函数可以沿着回溯链向上查找匹配的键值对。
- 只能找父亲节点,不能找兄弟节点
- 只能就近取值,如果自己和父母节点都存储了 key,只能找到自己这里。
- 利用 value() 函数逻辑沿着回溯链查找最近的 cancelCtx 祖先,用于构造 Context 树(这一点需要详细分析一下 parentCancelCtx 函数的代码,后续讲到树构建就会一目了然)。
go
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:
return c.Value(key)
}
}
}
parentCancelCtx:返回 parent 的第一个祖先 cancelCtx 节点。
go
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
// 返回第一个实现了 Done() 的实例
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
// 回溯链中寻找第一个 *cancelCtx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
// 判断回溯链中第一个实现 Done() 的实例是不是 cancelCtx 的实例
if pdone != done {
return nil, false
}
return p, true
}
理解该函数代码,主要是这一行会有疑惑:if pdone != done
,接下来详细解释一下: 因为只有 *cancelCtx 实现了非空 Done 方法,因此 done := parent.Done() 会返回第一个祖先 cancelCtx 中的 done channel,除非该 Context 回溯链中存在第三方实现的 Context 接口的实例,parent.Done() 才有可能返回其他 channel,如下图所示:假设 C3 是由我们自己实现 Context,parent.Done() 才会返回自己实现的 Done channel。
理解到这里,你也就看得懂 parentCancelCtx 函数的代码了,其实就是返回了 parent 的第一个祖先 cancelCtx 节点。为啥一定要找到第一个祖先 cancelCtx 节点呢?其实是为了进行 Context 树的构建,请看下一小节讲解。
3.2 Context 树
前文提到树形结构和 children map[canceler]struct{}
字段息息相关,主要作用是关联祖先节点和孩子节点之间的关系,最终用于在祖先节点取消时,级联取消所有孩子节点,接下来看一下树结果是如何构建和进行级联取消的。
3.2.1 树构建
Context 树的构建是在调用 context.WithCancel() 调用时通过 propagateCancel 进行的。调用过程为:WithCancel -> withCancel -> propagateCancel,具体看一下源码:
go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
// 回溯链
c := newCancelCtx(parent)
// 树构建
propagateCancel(parent, c)
return c
}
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(), Cause(parent))
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, p.cause)
} else {
// 懒汉式创建
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 建立树结构
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 祖先是第三方实现的 Context 节点
// 启动守护线程,在祖先取消时,取消该孩子节点
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
代码注释写的很详细,应该可以看懂,这里把重点讲解一下: done := parent.Done()
沿着回溯链找到了第一个实现 Done() 方法的实例,存在已下四种情况:
- done channel 为 nil,表示无法取消,那自然不用级联取消孩子;
- done channel 已经关闭,表示祖先已经取消,自然孩子之间触发取消就行;
- 既然祖先能取消&还未被取消,需要判断祖先是不是 *cancelCtx 节点,利用上一节提到的 parentCancelCtx 函数寻找,如果是 *cancelCtx 则必然实现了 canceler 接口,直接放入 map 中即可;
- 祖先不是 *cancelCtx 节点 & 还实现了非空 Done() 函数,那只能是三方自己实现的类了,因为不知道其内部实现,所以无法利用 map(有没有这个字段都不知道),只能开启一个守护协程,当祖先节点取消时,直接取消孩子节点。
propagateCancel 函数通过处理这四种情况,做到了只要祖先在能取消前提下,发生了取消,必然会触发孩子节点的取消。又有人会问了,第三种只是塞进去了,没看到触发取消呀,这个其实前面已经讲过一次了,我们来回顾一下 *cancelCtx 节点的 cancel 方法,也就是触发级联取消的地方。
3.2.2 级联取消
因为代码不长,就不做删减了。代码中通过 for range map 的方式对其所有的 child 节点做了 cancel,并通过 c.children = nil
的方式断绝了与所有孩子的联系;还通过 parentCancelCtx 函数找到了祖先节点,通过 delete(p.children, child)
进行了解绑,断绝了与祖先的联系。
go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
// 并发写锁
c.mu.Lock()
// err 不为 nil,表示已经被取消过,那则不用继续取消了
if c.err != nil {
// 解锁
c.mu.Unlock()
return // already canceled
}
// 设置取消原因和错误
c.err = err
c.cause = cause
// 取出 chan
d, _ := c.done.Load().(chan struct{})
// chan 未初始化
if d == nil {
// 直接存入已关闭的 chan,通知取消操作
c.done.Store(closedchan)
} else {
// 关闭 chan,通知取消
close(d)
}
// 祖先取消,孩子也得跟着取消(级联取消)
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
// 取消 child
child.cancel(false, err, cause)
}
// 断绝与所有后代的关系
c.children = nil
// 解锁
c.mu.Unlock()
// 如果想断绝与祖先的关系
if removeFromParent {
// 断绝与祖先的关系
removeChild(c.Context, c)
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
// 回溯找到第一个祖先 *cancelCtx
// 只有 *cancelCtx 中存在 children map[canceler]struct{},才有父子关系
p, ok := parentCancelCtx(parent)
// 没找到,意味着没有父子关系,不用断绝
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
// 祖先的 children map 中移除对该孩子的关系
delete(p.children, child)
}
p.mu.Unlock()
}
总结
阅读到这里,context 包的主要内容就串完了,这篇文章只讲述了 context 包的源码,但没有讲解具体如何使用(下一期继续),迫不及待的同学可以通过 pkg.go.dev 查询到 context 的官方说明文档:pkg.go.dev/context,里面记录了每个对外函数的使用案例。这里再啰嗦一下 Context 的使用规则:
- 不要将 Context 存储在结构类型中;相反,将 Context 显式传递给每个需要它的函数。Context 应该是第一个参数,通常命名为 ctx。
- 即使函数允许,也不要传递 nil Context 。 如果您不确定要使用哪个 Context, 请传递context.TODO 。
- 不要将函数的可选参数放在 context 当中,context 中一般只放一些全局通用的数据,例如 tracing id。
- 相同的 Context 可以传递给在不同 goroutine 中运行的函数,Context 对于多个 goroutine 同时使用是并发安全的。
本文详细的讲解了 context 包的源码,context 包主要用于处理并发请求、控制请求执行时间、取消操作、传递值等场景。它在处理微服务架构中的请求处理、HTTP 请求处理等方面非常实用。
理解 context 包源码最重要的是要理解其中的回溯链以及树结构,文章中已经使用详细的图进行了讲解;回溯链中四种实现类 emptyCtx、valueCtx、cancelCtx、timerCtx 完成了不同的功能:
- emptyCtx:用于根节点的创建,不可取消,返回 nil;
- valueCtx:用于传值,以 key-val 存储对应的键值对,注意只能回溯直系祖先,不能寻找兄弟节点,而且相同的 key 采取就近原则;
- cancelCtx:该类是核心类,实现了 canceler 接口,是一个可取消的 Context,也是唯一一个实现了非空 Done() 方法的结构体,结构体中字段 children 维护了 canceler 树的结构,用于级联取消孩子节点,完成整个分支的取消操作;而 Context 的取消操作是通过关闭一个只读 channel 进行广播通知的。
- timerCtx:该结构体中内嵌了 cancelCtx,并定义了定时器,用于定时取消。
以上就是本文的全部内容,如果觉得还不错的话欢迎点赞 ,转发 和关注,感谢支持。