背景
context
在整个Golang生态体系中被广泛使用,我们在平时开发的时候也会经常用到它,而且Golang的很多标准包都依赖它context
。
关于context的一些使用方法的应用场景我在前面两篇文章也做了详细介绍,今天这篇文章我们就来介绍context
内部的源码。
我们先回顾一下context
它能解决的一些问题:
- 公共参数或数据的传递
- 通知所有子goroutine优雅退出,从而释放资源
- 业务执行超时或者在截止日期之前执行结束,然后程序优雅地退出或返回
上面几种使用方法就不做详细的介绍,下面直接看context
包的源码。
源码分析
我们打开context包里面的context.go文件,发现其实除开注释的一些文档外,差不多也就只有200多行的代码。
Context接口和emptyCtx
context最基本的数据结构是Context如下接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
我们在使用上下文时,一般来说,第一步是使用context.Background()函数创建根上下文(上下文逐个链接在一起,形成树结构,根上下文是链中的第一个上下文),那么Background()
这个是怎么去创建的呢?如下:
var background = new(emptyCtx)
func Background() Context {
return background
}
Background
函数返回一个声明为background的全局变量new(emptyCtx)
,那么什么是emptyCtx,我们继续:
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
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 interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
可以看到emptyCtx
他其实是一个基于int
类型的自定义类型。
这里这个int
,其实对于context来说,不管 emptyCtx 是基于 int、string 还是其他什么类型并不重要,重要的是实现了接口 Context 中定义的所有四个方法都返回nil
,因此根上下文永远不会被取消,没有值,也没有截止日期
。
好了,我们继续往下看,在context.go这个文件里面的 valueCtx
和 WithValue
又是什么呢?
valueCtx 和 WithValue
上下文的一种典型用法是传递数据。在这种情况下,我们需要用到WithValue
这个方法,例如下面的例子:
rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "key", "value")
WithValue
是一个只有一个返回值的函数:
func WithValue(parent Context, key, val interface{}) 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}
}
这里有一个reflectlite
我们暂时先忽略他,后面在去做介绍。这里只需要关心WithValue
返回值类型是&valueCtx
:
type valueCtx struct {
Context
key, val interface{}
}
这里有一个 Golang 语言特性:embedding
,它实现了composition
。在这种情况下,valueCtx
中定义了Context的所有四种方法。
简单来说,embedding有 3 种类型:结构中的结构
、接口中的接口
以及结构体中的接口
,valueCtx
就属于最后一种。
当想取出值时,可以使用以下Value方法:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
如果这个key
参数与当前上下文的key不匹配,则将调用父上下文的 Value 方法
。如果我们仍然找不到这个key,那么父上下文会继续去调用它的父上下文,一直沿着链路向上去获取,直到根节点,根节点就会返回nil
,就像我们上面提到的emptyCtx
:
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
cancelCtx 和 WithCancel
首先我们来看下面这个示例:
package main
import (
"context"
"fmt"
"time"
)
func main() {
cancelCtx, cancelFunc := context.WithCancel(context.Background())
go task(cancelCtx)
time.Sleep(time.Second * 3)
cancelFunc()
time.Sleep(time.Second * 3)
}
func task(ctx context.Context) {
i := 1
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println(i)
time.Sleep(time.Second * 1)
i++
}
}
}
当主协程想要取消task这个协程的时候,只需调用cancelFunc
,然后 task 协程退出并停止运行,这样,goroutine的管理就会变得很容易。
我们来看一下他的源码:
type CancelFunc func()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
WithCancel
返回两个值,第一个是通过newCancelCtx
创建&c
(&cancelCtx{}),第二个是 CancenlFunc
类型,一个取消函数。
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
可以看到Context
也嵌入了cancelCtx
内部,同时还定义了其他几个字段,我们先来看Done()
这个方法:
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
Done()
返回通道done
,在上面的实例中,task goroutine 监听来自cancelCtx
的取消信号,如下所示:
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
...
该信号通过调用cancle函数
来触发,那么这个cancle内部具体是处理什么以及信号是如何发送到通道的。我们来看cancelCtx
的方法cancal
:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// set the err property when cancel is called for the first time
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
如cancelCtx
结构体所示,cancelCtx
有四个属性,然后通过cancel这个方法可以判断出这四个属性的作用:
mu
:一个通用锁,以确保 goroutine 安全并避免竞争条件;err
:表示cancelCtx
是否被取消的标志。当cancelCtx被创建时,err值为nil
,cancel第一次调用时,将通过c.err = err
设置done
:发送取消信号的通道,为了实现这一点,上下文只需关闭(close
)已完成的通道,而不是向其中发送数据。当我们的通道关闭后,接收者其实仍然可以根据通道类型从关闭的通道中获取零值
。children
:包含其所有子上下文的Map
。如果当前上下文被取消时,取消操作将通过在 for 循环中调用child.cancel(false, err)
传播给子级。那么这个父子关系是什么时候建立的呢?秘密就在propagateCancel()函数内部;
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
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()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
propagateCancel
里面建立父级和子级之间的关系,关键点是 parentCancelCtx
函数,用于查找最里面的可取消父级上下文:
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
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
}
可以看到Value
方法被调用,因为我们在上面分析过,Value
会传递搜索直到根上下文。
回到propagateCancel
函数,如果找到可取消的父级上下文,则将当前上下文添加到子级哈希map
中,如下所示:
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
到这里父子关系就已经建立了。
总结
在本文中,我们分析了Context
包的源代码,并了解 Context
、valueCtx
和 cancelCtx
的工作原理。
Context
还包含其他两种类型的上下文:timeOut
上下文和 deadLine
上下文,这两个我们在下一篇context源码第二部分
中去讨论。