0前言
context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制. 除此之外,context 还兼有一定的数据存储能力. 本着知其然知其所以然的精神,本文和大家一起深入 context 源码一探究竟,较为细节地对其实现原理进行梳理.
context.Context
Context 为 interface,定义了四个核心 api:
标准error
- Canceled:context 被 cancel 时会报此错误;
- DeadlineExceeded:context 超时时会报此错误.
原始父亲类emptyCtx
类的实现
emptyCtx相当于context的初始化鼻祖
各种类返回一个空值,在后面会讲到子context,用以区分。子context都是继承父context从而形成一个新的context。
创建一个父context方法 -> context.Background() & context.TODO()
我们所常用的 context.Background() 和 context.TODO() 方法返回的均是 emptyCtx 类型的一个实例.
子类数据结构 cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex // 用以协调并发场景下的资源获取
done atomic.Value // 实际类型为 chan struct{},即用以反映 cancelCtx 生命周期的通道;
children map[canceler]struct{} // 保存所有子类context
err error // 在第一次取消调用时设置为非零
}
type canceler interface {
cancel(removeFromParent bool, err error)关闭此context.具体实现后面会讲到
Done() <-chan struct{}//- 当context关闭后Done()返回一个被关闭的管道关闭的管理仍然是可读的据此goroutine可以收到关闭请求
//如果没有关闭返回nil
}
cancelCtx 未实现Deadline(过期)方法,仅是 embed 了一个带有 Deadline 方法的 Context interface,因此倘若直接调用会报错.
Done方法
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
- 基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
- 加锁后,在此检查 chan 是否存在,若存在则返回;(double check)
- 初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
Err方法
func (c *cancelCtx) Err() error {
c.mu.Lock() //加锁
err := c.err//读取cancelCtx.err
c.mu.Unlock()//解锁
return err //返回结果
}
Value 方法
通过ctx的key获取value
func (c *cancelCtx) Value(key any) any {
//倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
if key == &cancelCtxKey {
return c
}
//否则遵循 valueCtx 的思路取值返回
return value(c.Context, key)
}
context.WithCancel()为父ctx创建一个子context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
//校验父 context 非空;
if parent == nil {
panic("cannot create context from nil parent")
}
//校验父 context 非空;
c := newCancelCtx(parent)
//在 propagateCancel 方法内启动一个守护协程,以保证父 context 终止时,该 cancelCtx 也会被终止;
propagateCancel(parent, &c)
//将 cancelCtx 返回,连带返回一个用以终止该 cancelCtx 的闭包函数.
return &c, func() { c.cancel(true, Canceled) }
}
newCancelCtx为父ctx创建一个新的子ctx
func newCancelCtx(parent Context) cancelCtx {
//注入父 context 后,返回一个新的 cancelCtx.
return cancelCtx{Context: parent}
}
propagateCancel()用以传递父子 context 之间的 cancel 事件
func propagateCancel(parent Context, child canceler) {
//倘若 parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回;
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
//倘若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;否则什么都不干
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
//假如 parent 是 子ctx 的类型,则加锁,并将子 context 添加到 父ctx 的 children map 当中;
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()
//假如 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户
//自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并透传 parent 的 err.
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
进一步观察 parentCancelCtx 是如何校验 parent 是否为 子ctx 的类型:
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
////倘若 parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
if done == closedchan || done == nil {
return nil, false
}
//倘若以特定的 cancelCtxKey 从 parent 中取值,取得的 value 是 parent 本身,则返回 true. (基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议).
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}