[golang] 2 - context

Posted by Dongbo on March 6, 2022

用golang有一段时间了,但是一直以来都是要用什么就查什么,时间一长啥也记不住。所以计划一点点归纳接触到的各种golang机制和标准库的用法,加深印象,也方便查阅。

在项目里接触过一些 context 的用法,之前只知道能够用来同步终止多个goroutine,一般来说我们使用 context 的方式如下面代码片段所示:

1
2
3
4
5
6
7
8
func foo(ctx Context, args interface) {
	select {
	case <- ctx.Done():
		return 
	default:
		// do something
	}
}

现在来扒一下源码看看标准库如何实现 context的功能。首先 context 接口有以下四个方法:

1
2
3
4
5
6
7
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

并且有四个用来创建 context 的方法:

1
2
3
4
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func 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

可以看到Done()函数返回的是一个channel,实际上 context 的 cancel 操作就是通过关闭其内部的 channel 来完成的。当 context 被手动取消或者超时取消后,内部 chennel 关闭,通知所有使用当前 context 及其 child context 的 goroutine 进入生命周期结束的处理逻辑(一般用法是如此)。

context 提供两个函数 Background(), TODO() 来创建一个 emptyContext,主进程可以通过这两个函数来创建 root context。顺带一提这里的 emptyContext 其实就是通过 type emptyContext int 随后给该类型实现了 context 接口(但是函数里没有做任何操作)。

目前我用得比较多的是 WithCancel()WithTimeout(),下面具体介绍一下他们的实现。

WithCancel

WithCancel 中创建的 cancelCtx 才是第一个具备实际 cancel 功能的类型。后续的 WithTimeoutWithDeadline 都是复用它的 cancel 实现来做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

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
}

代码片段中的propagateCancel函数会把当前创建的 context 加入 parent 的 children列表中;innermost 的 cancelCtx 还会启动一个 goroutine 检测 parent 是否结束运行,当发现 parent.Done() 之后,也将调用child的cancel函数中止 child context。

cancelCtx 不会自动进行 cancel,需要我们手动调用 cancel 来进行取消。一般来说可以用 defer 保证退出运行时执行 cancel 函数。

WithDeadline

WithTimeout 封装了一下 WithDeadline,相当于 WithDeadline(parent, time.Now().Add(timeout)),所以这里我们还是来看 WithDeadline 的实现。

返回的是一个 timerCtx 内含一个 timer,超时之后自动调用 cancel。如果 child 的超时时间晚于 parent,则设为与 parent 一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

可以看到 timerCtx 的 cancel 也是复用 cancelCtx 的,除了 timer 以外没有增添其他东西。

WithValue不懂,懒得看了。

The End