Go 的 context
包是并发编程的核心工具,他的设计通过统一接口简化了 goroutine 的生命周期管理。其实核心就在于 context.Context
接口的四个方法:Deadline()
、Done()
、Err()
和 Value(key)
。但是呢 cancelCtx
和 timerCtx
是其实现的关键结构,分别用于处理主动取消和超时取消。
取消流程
cancelCtx
是支持取消操作的核心结构,源码:
1 | type cancelCtx struct { |
done channel
当调用 cancel()
方法的时候,他会首先会标记错误状态(比如说 Canceled
或 DeadlineExceeded
),并通过 closedchan
去关闭 done
channel。具体步骤如下:
- 原子操作保护 :使用互斥锁
mu
确保并发安全,防止多次调用cancel()
。 - 错误状态标记 :将
err
字段设置为指定错误值(如Canceled
),后续调用Err()
会返回该值。 - 关闭
done
channel :通过closedchan
(一个已关闭的空结构体 channel)替换原done
channel,触发响应 。
取消子节点
cancelCtx
还维护了一个子节点集合(children map[canceler]struct{}
),当调用 cancel()
时会遍历所有子节点并递归调用它们的 cancel()
方法。简单来说只要执行了这个,那么就能确定取消信号能在父节点之间传播下去了:
1 | for child := range c.children { |
子节点类型 :子节点可能是
cancelCtx
或timerCtx
,均实现canceler
接口。传播机制 :递归调用保证父节点取消时,所有子节点同步被取消
从父节点删除自己
假设当前 cancelCtx
的父节点也是可取消的contetxt(如 cancelCtx
或 timerCtx
),那么就去调用 removeChild
从父节点的子节点集合中把自己删掉,释放资源:
1 | if p, ok := c.Context.(*cancelCtx); ok { |
懒加载
done
channel 用的是懒加载策略(通过 atomic.Value
存储),只会在首次调用 Done()
的时候去初始化。通过这个方式可以优化内存占用,但是还是需要在并发访问时确保原子性 。
timerCtx
的超时处理
timerCtx
直接继承了 cancelCtx
,通过增加一个定时器字段就可以实现超时取消:
1 | type timerCtx struct { |
WithDeadline
当调用 context.WithDeadline(parent Context, deadline time.Time)
的时候,发生了:
- 计算剩余时间 :假设
deadline
过了,那么立即取消;否则计算剩余时间d := t.deadline.Sub(now)
。 - 启动定时器 :创建一个
time.Timer
,在d
时间后触发cancel()
方法:
1 | t.timer = time.AfterFunc(d, func() { |
- 传播机制 :超时触发后,
timerCtx
会调用cancel()
,继承cancelCtx
的递归取消逻辑。
提前取消与资源释放
如果说定时器触发前就调用 cancel()
的话,就得手动停止定时器以避免资源泄漏:
1 | func (c *timerCtx) cancel(removeFromParent bool, err error, manual bool) { |
这个里面还涉及了竞态处理:
- 竞态处理 :
Stop()
返回布尔值,确保定时器未触发时才释放资源,避免 race condition。
总结
context
的设计通过接口抽象还有组合模式实现了简洁高效的并发控制。cancelCtx
的递归取消和 timerCtx
的定时器机制共同实现了 Go 并发模型的核心能力。
当然了你如果不想用的话也是可以手动传递 channel 实现取消通知之类的方法啦。