Go 的 context 包是并发编程的核心工具,他的设计通过统一接口简化了 goroutine 的生命周期管理。其实核心就在于 context.Context 接口的四个方法:Deadline()Done()Err()Value(key)。但是呢 cancelCtxtimerCtx 是其实现的关键结构,分别用于处理主动取消和超时取消。

取消流程

cancelCtx 是支持取消操作的核心结构,源码:

1
2
3
4
5
6
7
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map[canceler]struct{}
err error
}

done channel

当调用 cancel() 方法的时候,他会首先会标记错误状态(比如说 CanceledDeadlineExceeded),并通过 closedchan 去关闭 done channel。具体步骤如下:

  • 原子操作保护 :使用互斥锁 mu 确保并发安全,防止多次调用 cancel()
  • 错误状态标记 :将 err 字段设置为指定错误值(如 Canceled),后续调用 Err() 会返回该值。
  • 关闭 done channel :通过 closedchan(一个已关闭的空结构体 channel)替换原 done channel,触发响应 。

取消子节点

cancelCtx 还维护了一个子节点集合(children map[canceler]struct{}),当调用 cancel() 时会遍历所有子节点并递归调用它们的 cancel() 方法。简单来说只要执行了这个,那么就能确定取消信号能在父节点之间传播下去了:

1
2
3
for child := range c.children {
child.cancel(false, err, false)
}
  • 子节点类型 :子节点可能是 cancelCtxtimerCtx,均实现 canceler 接口。

  • 传播机制 :递归调用保证父节点取消时,所有子节点同步被取消

从父节点删除自己

假设当前 cancelCtx 的父节点也是可取消的contetxt(如 cancelCtxtimerCtx),那么就去调用 removeChild 从父节点的子节点集合中把自己删掉,释放资源:

1
2
3
4
5
6
7
if p, ok := c.Context.(*cancelCtx); ok {
p.mu.Lock()
if s, ok := p.children[c]; ok {
delete(p.children, c)
}
p.mu.Unlock()
}

懒加载

done channel 用的是懒加载策略(通过 atomic.Value 存储),只会在首次调用 Done() 的时候去初始化。通过这个方式可以优化内存占用,但是还是需要在并发访问时确保原子性 。

timerCtx 的超时处理

timerCtx 直接继承了 cancelCtx,通过增加一个定时器字段就可以实现超时取消:

1
2
3
4
5
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}

WithDeadline

当调用 context.WithDeadline(parent Context, deadline time.Time) 的时候,发生了:

  • 计算剩余时间 :假设 deadline 过了,那么立即取消;否则计算剩余时间 d := t.deadline.Sub(now)
  • 启动定时器 :创建一个 time.Timer,在 d 时间后触发 cancel() 方法:
1
2
3
t.timer = time.AfterFunc(d, func() {
t.cancel(true, DeadlineExceeded, false)
})
  • 传播机制 :超时触发后,timerCtx 会调用 cancel(),继承 cancelCtx 的递归取消逻辑。

提前取消与资源释放

如果说定时器触发前就调用 cancel() 的话,就得手动停止定时器以避免资源泄漏:

1
2
3
4
5
6
func (c *timerCtx) cancel(removeFromParent bool, err error, manual bool) {
c.cancelCtx.cancel(removeFromParent, err, manual)
if c.timer.Stop() {
// 定时器未触发,释放资源
}
}

这个里面还涉及了竞态处理:

  • 竞态处理Stop() 返回布尔值,确保定时器未触发时才释放资源,避免 race condition。

总结

context 的设计通过接口抽象还有组合模式实现了简洁高效的并发控制。cancelCtx 的递归取消和 timerCtx 的定时器机制共同实现了 Go 并发模型的核心能力。

当然了你如果不想用的话也是可以手动传递 channel 实现取消通知之类的方法啦。