目录

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。 ./pic1.png

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中查询,若没有找到,则依次向上查找,通过查看代码得知,是采用深度遍历。