Golang中context笔记
一. context定义
context是go并发编程中经常用到的编程模式, 旨在实现以下机制:
- 对于并发的层层任务, 做到上层任务取消之后, 所有的下层任务都会被取消
- 中间的某层任务取消后, 只会将当前任务和其下层任务取消, 而不会影响到上层任务以及同级任务
注解: 我们将进程之间的串行抽象为一棵树, 那么每个进程就相当于树的节点, context就是每个节点对应的标识, 可以用于附带某些信息
二. context包中的类与函数
1. context接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()
Time类是time包下的类, 用来保存和传递时间, 可以使用Before/After/Equal来进行比较
无参, 返回一个Time类, 以及一个bool值
这里返回的Time是绑定当前context的任务被取消的截止时间, 如果没有设定截止时间, 就会返回ok == false
Done()
无参, 返回一个只读的管道, 当绑定当前的context的任务被取消时, 返回一个关闭的channel管道, 否则返回一个nil
一般在select-case中使用
Err()
如果Done()返回的channel没有关闭, 将返回nil, 否则返回非空的error代表任务结束的原因
比如取消:
Canceled, 超时:DeadlineExceeded
Value(key interface{}) interface{}
返回context存储的键值对中当前key 对应的 value, 如果没有的话返回nil
注解: 这里或许有点懵, 但是暂且先记住, 看到下面的接口实现, 自然会明白这些抽象方法的作用
2. emptyCtx 变量
emptyCtx是一个struct{}空结构体变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。
// 可以看出是空结构体
type emptyCtx struct{}
// 实现了Context接口
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
}
// 两种特殊的emptyCtx, 设置为私有
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
// 提供get方法
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
但我们一般不会直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量,分别可以通过调用Background和TODO方法得到,但这两个context在实现上是一样的。那么Background和TODO方法得到的context有什么区别呢?
Background 一般用于主函数, 初始化, 或者测试中, 作为context树的根节点
TODO一般用作不确定使用什么类型的context的时候才会使用, (下面会讲context的种类)
总而言之, 在下面的不同的context种类被创建时一般都会传入一个父节点, 我们会根据场景来选择background或者todo
3. cancelCtx
3.1 canceler
首先定义一个接口canceler
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
cancel(removeFromParent bool, err, cause error)
传入一个bool值, 一个error, 前者代表是否删除本身这个context, 后者就是删除的原因
如果实现了该接口, 就说明是可以取消的
3.2 cancelCtx
type cancelCtx struct {
Context //嵌入接口类型,cancelCtx 必然为某个context的子context;
mu sync.Mutex // 互斥锁,保护以下字段不受并发访问的影响
done atomic.Value // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
children map[canceler]struct{} // 其实是一个set, 保存当前context的所有子context, 也就是一个节点的子节点, 第一次取消调用时设为nil
err error // 第一次取消时设置为取消的错误原因
cause error // 同上
}
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{})
}
c.done 使用了惰性加载机制, 只有第一次调用Done() 方法时才会被创建, 且通过加锁二次检查, 保证了多个goroutine同时调用Done() 方法时, 只有第一个可以创建管道
函数返回一个只读的channel, 当其关闭时, 就会立即读出零值, 据此可以判断cancelCtx是否被取消
Err()方法
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
Value()方法
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
当key = &cancelCtxKey时, 就会返回他本身, 否则就会一直往上找
3.3 WithCancel() 函数
可以创建一个cacelCtx
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 := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
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()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent
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:
}
if p, ok := parentCancelCtx(parent); ok {
// parent is a *cancelCtx, or derives from one.
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()
return
}
if a, ok := parent.(afterFuncer); ok {
// parent implements an AfterFunc method.
c.mu.Lock()
stop := a.AfterFunc(func() {
child.cancel(false, parent.Err(), Cause(parent))
})
c.Context = stopCtx{
Context: parent,
stop: stop,
}
c.mu.Unlock()
return
}
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
这里主要聚焦四个函数
cancel方法, 绑定到cancelCtx指针上, 代表实现了canceler接口, 作用是取消这个节点, 设置上err和cause, 同时传入的bool代表是否删除该节点, 方法内还关闭了管道, 然后继续向子context传递取消信息
propagateCancel() 方法, 用以传递父子 context 之间的 cancel 事件,向上寻找可以挂靠的可取消的context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。
withCancel() 私有方法, 创建一个cancelCtx{}, 然后返回, 同时调整当前的context, 向上寻找可以挂靠的可取消的context
WithCancel() 向外提供的方法, 创建一个cancelCtx{}, 同时提供一个function, 用来取消当前的context
若 parent 是不会被 cancel 的类型(如 emptyCtx),不需要传递,则直接返回;
若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;
若 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;
若 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err
WithCancelCause(parent Context)
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
也是创建一个cancelCtx, 同时返回一个function用于取消, 但是这里取消的时候可以返回一个错误原因, 比较好用
4. timerCtx
timerCtx在cancelCtx基础上又做了一层封装,除了继承cancelCtx的能力之外,新增了一个time.Timer用于定时终止context;另外新增了一个deadline字段用于字段timerCtx的过期时间.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
4.1 继承方法
Deadline()
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
可见就是将其deadline返回
cancel(removeFromParent bool, err, cause error)
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
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()
}
直接使用父类的cancel方法, 然后判断是否需要从父亲的儿子列表中移除, 当都不需要的时候, 加锁, 将计时器停止并置空, 然后解锁返回
4.2 WithDeadlineCause
func WithDeadlineCause(parent Context, d time.Time, cause error) (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{
deadline: d,
}
//找到上一个可以取消的context, 接上去
c.cancelCtx.propagateCancel(parent, c)
// 计算一下现在到截止时间还有多少时间, 如果小于等于0说明已经到达截止时间, 就要直接cancel掉
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil { // 开启倒计时
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
context包一共提供了四种创建的方法
WithDeadlineCause, WithDeadline, WithTimeout, WithTimeoutCause
如果带Cause就是带有结束原因
这四种创建方式都是对WithDeadlineCause的复用
5. ValueCtx
context的一种, 可以存储信息
type valueCtx struct {
Context
key, val any
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
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 withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
- 可以看出实现了
context接口, 并且这是个私有类 - 实现了
Value方法, 用以在context链路上获取val. 如果当前context上不存在需要的key, 就会一直沿着父亲向上搜索, 当然如果搜索到根节点background还是没有, 那么就直接返回nil(可以看emptyCtx实现的Value方法), 当然如果遇到对应的key, 就会返回查找到的val
所以可以看出, 这是一个递归的查找问题, 不断地跳父亲节点 - 其中
cancelCtx、timerCtx、emptyCtx类型会有特殊的处理方式,如当前key== &cancelCtxKey,则会返回cancelCtx自身,而emptyCtx则会返回空
5.1 常用函数
WithValue()函数
用以向context添加键值对
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}
}
传入一个父节点context, 然后对应的key和val, 在判断完key是否为空以及是否可比较之后, 就会创建一个valueCtx对象, 相当于连接到传入的context后面
这就是一个链式结构
三.总结

- 这里只是列举了常用的一些
context类, 其实还有很多 - 不要在结构类型中加入
Context参数,而是将它显式地传递给需要它的每个函数,并且它应该是第一个参数,通常命名为 ctx: Context是线程安全的,可以放心地在多个 goroutine 中使用。- 当你把
Context传递给多个goroutine使用时,只要执行一次cancel操作,所有的goroutine就可以收到 取消的信号 - 不要把原本可以由函数参数来传递的变量,交给
Context的Value来传递。 - 当一个函数需要接收一个
Context时,但是此时你还不知道要传递什么Context时,可以先用context.TODO来代替,而不要选择传递一个nil。 - 当一个
Context被cancel时,继承自该Context的所有 子Context都会被cancel。

浙公网安备 33010602011771号