前言
context 是一个很常用的功能,并且源码也不那么难。
我觉得是比较适合初次入手的,整个代码564行,去掉注释,估计200-300行。并且没有特别涉及到底层的代码。
适合做一个好的开头,如果一开始就读GC源码,感觉就是直接能放弃~
用途上下文 context.Context 是用来设置截止日期、同步信号,传递请求相关值的结构体。
它适合在多 goroutine 的情况下,管理 goroutine 直接的关系。
如果没有这个模块,我们在不断 go goroutine 时,goroutine 会失控,比如父线程其实已经终止了,但是这个父线程启动的子线程可能还在占用资源。
比较直接的方式,起启动一个协程池,去标记管理启动的 goroutine,但是这种就比较偏自由的实现了,实现方案和代码能力强相关。
golang源码也提供了解决方案,context.Context 就是一个标准的,管理 goroutine 之间的关系模块。
他的底层是通过 channel 去传递管理信号。例如,父 goroutine 可以传递一个close信号,子 goroutine 通过管道监听这个消息,如果收到就自动关闭。
提供了下面4中标准方法:WithCancel
WithDeadline
WithTimeout
WithValue
其中 WithDeadline 和 WithTimeout其实一个实现,只是用法有差别
源码里面可以看出 WithTimeout 是把 WithDeadline 包装了一层
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
前面3个方法是主要通过通道去管理 goroutine , WithValue 这是可以在父子 goroutine 共享内容。
使用场景也非常广,例如 gin 这种web框架,就包装了Context,并且作为请求处理函数的第一个参数,供开发者使用。
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
gin.Context 就是对 context.Context 的一个封装。
实现
这个源码很早看过,当时看着别人的源码解读,很快有个大致的思路,但是真正在我要写这篇文章,自己描述时,发现其实理解不到位。
所以这个源码,我打算通过如果一步步自从自己最简单的实现去剖析源码。
WithCancel
一个最简单的应用,
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 3; i++ {
go func(ctx context.Context, index int) {
select {
case
fmt.Println(index)
}
}(ctx, i)
}
cancel()
time.Sleep(1 * time.Second)
}
当主 goroutine 调用 cancel() 后,子 goroutine 会收到
假如我们想自己实现这个逻辑,怎么实现
最简版
func main() {
var ctx = make(chan int)
for i := 0; i < 3; i++ {
go func(ctx chan int, index int) {
select {
case
fmt.Println(index)
}
}(ctx, i)
}
close(ctx)
time.Sleep(1 * time.Second)
}
上述代码通过 chan 可以实现一样的功能,并且 context 底层也是通过这种方式去实现的。
核心其实就是关闭通道 close 。
更进一步
我们更进一步,实现的更像 context 一点,主要是按照 context 思路,实现了Done 和 cancel 两个功能。
type Cont struct {
mu sync.Mutex
done chan struct{} // 这个就是上面例子里面的 chan 用于接收关闭信号, created lazily, closed by first cancel call}
func (c *Cont) Done()
c.mu.Lock()
// created lazily,如果有 chan 使用已有的,如果没有,创建一个,并且其他也会使用这个 if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *Cont) cancel() {
// 在关闭通道时,如果压根没有子 goroutine ,就直接赋值一个已经关闭的通道,这个处理很有技巧 // 这段代码中,其实只有在执行 ctx.Done() 时,才会去 created lazily 方式初始化一个 chan // 因为 goroutine 不确定执行顺序,可能在子 goroutine 还没有执行时,父就调用了 cancel 这个时候 c.done == nil // 所以这里就给 c.done 复制了一个关闭的 chan ,这样在子 goroutine 调用 ctx.Done() 时,就会马上收到一个关闭信号 c.mu.Lock()
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
c.mu.Unlock()
}
// 初始化一个关闭的 chanvar closedchan = make(chan struct{})
func init(){
close(closedchan)
}
func main() {
var ctx = Cont{}
for i := 0; i < 3; i++ {
go func(ctx *Cont, index int) {
select {
case
fmt.Println(index)
}
}(&ctx, i)
}
ctx.cancel()
time.Sleep(1 * time.Second)
}
基于上面简单的实现,大家在看源码,就能稍微清晰一点
WithDeadline
在看下 context.WithDeadline 的一个应用
func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1* time.Second))
for i := 0; i < 3; i++ {
go func(ctx context.Context, index int) {
select {
case
fmt.Println(index)
}
}(ctx, i)
}
time.Sleep(3 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
设置了一个1秒的超时,子线程会在1秒后自动关闭
这个也是在前面自己实现的基础上,加上了 time.AfterFunc ,下面是参考源码的一个简单实现
type Cont struct {
mu sync.Mutex
done chan struct{}
timer *time.Timer
}
func (c *Cont) Done()
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *Cont) cancel() {
c.mu.lock()
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 主动关闭计时器如果有的话 if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
var closedchan = make(chan struct{})
func init(){
close(closedchan)
}
// 这里主要加了 time.AfterFunc 做了一个超时自动关闭逻辑func Deadline(c *Cont, d time.Time) *Cont {
dur := time.Until(d)
c.mu.Lock()
defer c.mu.Unlock()
c.timer = time.AfterFunc(dur, func() {
c.cancel()
})
return c
}
func main() {
var ctx = Deadline(&Cont{}, time.Now().Add(1*time.Second))
for i := 0; i < 3; i++ {
go func(ctx *Cont, index int) {
select {
case
fmt.Println(index)
}
}(ctx, i)
}
time.Sleep(3 * time.Second)
}
WithValue
context.WithValue 这个相对前面,简单一点,源码也不多,可以直接看
主要是下面2段源码
func WithValue(parent Context, key, val interface{}) Context {
...
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
// 没有取得值,会向父层级迭代继续查找 return c.Context.Value(key)
}
深入研究
其实基于上面的实现,去源码对照阅读,发现源码里面还有一些父子关系的处理,我们并没有用到,例如 propagateCancel 函数。
上面的例子,也只是用上了一层父子关系逻辑,下面我们继续深入看看,多层关系的实现,网上这块用法其实比较少,我们先看看怎么实现。
多个层级的例子
func hander(ctx context.Context, name string) {
for i := 0; i < 3; i++ {
go func(ctx context.Context, index int) {
select {
case
fmt.Println(name, index)
}
}(ctx, i)
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
actx, acancel := context.WithCancel(ctx)
bctx, bcancel := context.WithCancel(ctx)
cctx, _ := context.WithCancel(ctx)
hander(actx, "a hander")
hander(bctx, "b hander")
hander(cctx, "c hander")
fmt.Println("close a hander")
acancel()
time.Sleep(2 * time.Second)
fmt.Println("close b hander")
bcancel()
time.Sleep(2 * time.Second)
fmt.Println("close other hander")
cancel()
time.Sleep(1 * time.Second)
}
输出
close a hander
a hander 0
a hander 1
a hander 2
close b hander
b hander 2
b hander 1
b hander 0
close other hander
c hander 2
c hander 1
c hander 0
这里我们看源码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
...
// 实例化一个子 context c := newCancelCtx(parent)
// 父和子绑定关系 propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
其中 newCancelCtx 这个逻辑,我们上面简单的实现就是参考这个去做的
所以可以重点看下 propagateCancel
func propagateCancel(parent Context, child canceler) {
...
if p, ok := parentCancelCtx(parent); ok {
// 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号; p.mu.Lock()
if p.err != nil {
// 如果已经被取消,child 会立刻被取消; child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号; p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
// 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel // 在 parent.Done() 关闭时调用 child.cancel 取消子上下文; go func() {
select {
case
child.cancel(false, parent.Err())
case
}
}()
}
}