context
为什么要有context
为了在协程之间传递信息,比如网络服务,每个请求都是一个协程,在协程中还可能开启新的子协程,子协程中还有新的协程,我们如果要取消请求的计算,所有的子协程都应该停止。
Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。最重要的是它是并发安全的。
context使用
我们知道context是可以形成一个树形结构,context是可以有父和多个子的。
我们一般用context.Background()和context.TODO()返回值来作为根root。
//一般用于取消子协程
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){}
//一般用于给定时间点取消子协程,当然也可以在deadline前主动去取消
WithDeadline(parent Context, d time.Time) (Context, CancelFunc){}
//和上面的类似,只不过不是特定的时间点,而是一段时间后取消
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc){}
//一般用于在协程间传递数据
func WithValue(parent Context, key, val interface{}) Context{}
上面的4个方法返回context,主要分为取消用和传值用,context一般用于参数传给协程
func main() {
deadline, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
go test1(deadline)
cancelFunc()
time.Sleep(7 * time.Second)
}
func test1(ctx context.Context) {
for {
select {
case <-ctx.Done(): //context到期或主动取消时,channel就被关闭,零值被取出,语句得到执行
fmt.Println(ctx.Err() == context.DeadlineExceeded)
fmt.Println("test1 stop")
return
default:
fmt.Println("test1")
time.Sleep(time.Second)
}
}
}
这个演示固定时间点自动取消子协程的代码。
context原理
相关接口
context.Context是一个接口,比较简单:
type Context interface {
//获取设置的截止时间
Deadline() (deadline time.Time, ok bool)
//返回只读通道,取消后即可从通道中读数据
Done() <-chan struct{}
//返回取消原因
Err() error
//根据key取到conetext中设置的值
Value(key interface{}) interface{}
}
Context接口并不需要我们实现,Go内置emptyCtx结构体已经帮我们实现了,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。
context.Background()和context.TODO()返回的都是emptyCtx。
Done()方法返回的是一个只读的channel,被取消的时候,channel会被父关闭,这样<-channel会返回零值,在此之前一直被阻塞。
canceler 再来看另外一个接口:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。 父被取消的时候会调用所有子的取消方法,哪怕子还没有到预定的时间。
相关结构体
emptyCtx 源码中定义了 Context 接口后,并且给出了一个实现:
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
}
这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。
cancelCtx 再来看一个重要的 context:
type cancelCtx struct {
Context
// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。
我们调用context.WithCancel(parent)方法时,生成一个cancelCtx,里面的Context字段就是context,然后判断父parent是不是也是cancelCtx类型,如果是,就把自己加到父的children中,这样子能找到父,父能找到子。 先来看 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
}
c.done 是“懒汉式”创建
接下来,我们重点关注 cancel() 方法的实现:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()
if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}
cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。
timerCtx timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx 首先是一个 cancelCtx,所以它能取消
创建 timerCtx 的方法:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout 函数直接调用了 WithDeadline,传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。也就是说,WithDeadline 需要用的是绝对时间。重点来看它:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
return WithCancel(parent)
}
// 构建 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// 挂靠到父节点上
propagateCancel(parent, c)
// 计算当前距离 deadline 的时间
d := time.Until(deadline)
if d <= 0 {
// 直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// d 时间后,timer 会自动调用 cancel 函数。自动取消
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
也就是说仍然要把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消。
如果要创建的这个子节点的 deadline 比父节点要晚,也就是说如果父节点是时间到自动取消,那么一定会取消这个子节点,导致子节点的 deadline 根本不起作用,因为子节点在 deadline 到来之前就已经被父节点取消了。
个函数的最核心的一句是:
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
c.timer 会在 d 时间间隔后,自动调用 cancel 函数,并且传入的错误就是 DeadlineExceeded:
context通过withX操作可以嵌套,形成一个树,新生成的context继承父的属性和特点,对父进行取消,父及子context均取消。
valueCtx
type valueCtx struct {
Context
key, val interface{}
}
由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 context。但它仍然是一个 Context。 它实现了两个方法:
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
创建 valueCtx 的函数:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
从创建函数可以看出,valueCtx和cancelCtx不同,他可以找到父亲,父亲并不能找到他。 取值的时候先在当前context中查询,若没有找到,则依次向上查找,通过查看代码得知,是采用深度遍历。