深刻理解Go Context

在Go语言并发编程中,用一个goroutine来处理一个任务,而它又会建立多个goroutine来负责不一样子任务的场景很是常见。以下图git

这些场景中,每每会须要在API边界之间以及过程之间传递截止时间、取消信号或与其它请求相关的数据github

谁是性能卡点呢?得通知它们任务取消了。golang

这时候就能够使用Context了。context包在Go1.7的时候被加入到官方库中。sql

context包的内容能够归纳为,一个接口,四个具体实现,还有六个函数。编程

Context接口提供了四个方法,下面是Context的接口安全

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

emptyCtx类型

emptyCtx本质上是一个整型, *emptyCtx对Context接口的实现,只是简单的返回nil,false,实际上什么也没作。以下代码所示:并发

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
}

Background和TODO这两个函数内部都会建立emptyCtx函数

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

其中Background主要用于在初始化时获取一个Context(从代码中可知本质是一个*emptyCtx,而emptCtx本质上是一个Int),这就是Background()函数返回的变量结构。性能

而TODO()函数,官方文档建议在原本应该使用外层传递的ctx而外层却没有传递的地方使用,就像函数名称表达的含义同样,留下一个TODO。spa

cancelCtx类型

再来看cancelCtx类型,cancleCtx定义以下

// cancelCtx能够被取消。 取消后,它也会取消全部实现取消方法的子级。
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
}

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

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
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

这是一种可取消的Context,done用于获取该Context的取消通知,children用于存储以当前节点为根节点的全部可取消的Context,以便在根节点取消时,能够把它们一并取消,err用于存储取消时指定的错误信息,而这个mu就是用来保护这几个字段的锁,以保障cancelCtx是线程安全的。

而WithCancel函数,能够把一个Context包装为cancelCtx,并提供一个取消函数,调用它能够Cancel对应的Context

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) }
}

示例代码:

ctx := context.Background()
ctx1, cancel := context.WithCancel(ctx)

timerCtx类型

再来看timerCtx,timerCtx定义以下

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

它在cancelCtx的基础上,又封装了一个定时器和一个截止时间,这样既能够根据须要主动取消,也能够在到达deadline时,经过timer来触发取消动做。

要注意,这个timer也会由cancelCtx.mu来保护,确保取消操做也是线程安全的。

经过WithDeadline和WithTimeout函数,均可以建立timerCtx,区别是WithDeadline函数须要指定一个时间点,而WithTimeout函数接收一个时间段。

接下来,咱们基于ctx1构造一个timerCtx

ctx := context.Background()
ctx1, cancel := context.WithCancel(ctx)

deadline := time.Now().Add(time.Second)
ctx2, cancel := context.WithDeadline(ctx1, deadline)

这个定时器会在deadline到达时,调用cancelCtx的取消函数,如今能够看到ctx2是基于ctx1建立的,而ctx1又是基于ctx建立的,基于每一个Context能够建立多个Context,这样就造成了一个Context树,每一个节点均可以有零个或多个子节点,可取消的Context都会被注册到离它最近的、可取消的祖先节点中。对ctx2来讲离它最新的、可取消的祖先节点是ctx1

因此在ctx1这里的children map中,会增长ctx2这组键值对

若是ctx2先取消,就只会影响到以它为根节点的Context,而若是ctx1先取消,就能够根据children map中的记录,把ctx1子节点中全部可取消的Context所有Cancel掉。

最后来看valueCtx类型

valueCtx类型

首先来看valueCtx的定义

type valueCtx struct {
	Context
	key, val interface{}
}

它用来支持键值对打包,WithValue函数能够给Context附加一个键值对信息,这样就能够经过Context传递数据了

var keyA string = "keyA"
ctx := context.Background()
ctxA := context.WithValue(ctx, keyA, "valA")

如今咱们给ctx附加一个键值对keyA=>valA,变量ctxA也是Context接口类型,动态类型为*valueCtx,data指向一个valueCtx结构体,第一个字段是它的父级Context,key和val字段都是空接口类型,keyA的动态类型为string,动态值是string类型的变量keyA,val的动态类型一样是string,动态值为valA,

下面咱们再基于ctxA,附加一个key相等但val不相等的键值对keyA=>eggo,ctxC的动态值指向这样一个valueCtx,父级Context天然是ctxA,key与ctxA中的相同,可是val的值与ctxA中的不相等

经过ctxC获取kyA和keyC对应的值时会发现keyC覆盖了keyA对应的val,要找到缘由,就要先看看Value方法是怎么工做的

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

首先它会比较当前Context中的key是否等于要查找的key,由于keyA等于keyC,因此对keyA的查找会直接锁定到ctxC这里的val,于是出现了子节点覆盖父节点数据的状况,为了规避这种状况,最好不要直接使用string、int这些基础类型做为Key,而是用自定义类型包装一下,就像下面这样,把keyA定义为keytypea类型,keyC定义为keytypec类型,这样再次经过ctxC获取keyA时,由于key的类型不相同,第一步key相等性比较不经过,就会委托父节点继续查找,进而找到正确的val

因此说valueCtx之间经过Context字段造成了一个链表结构,使用Context传递数据时还要注意,Context自己本着不可改变(immutable)的模式设计的,因此不要试图修改ctx里保存的值,在http、sql相关的库中,都提供了对Context的支持,方便咱们在处理请求时,实现超时自动取消,或传递请求相关的控制数据等等

了解了context包中,一个接口,四种具体实现,以及六个函数的基本状况,有助于咱们理解Context的工做原理

整理自:

context源码

幼麟实验室

相关文章
相关标签/搜索