# Golang Context
参考:
- https://juejin.im/post/5e52688c (opens new window)
- http://c.biancheng.net/view/5714.html
context
是 Golang 并发编程中常用到的一种编程模式,它主要用于同一任务不同步骤、父子任务和后台任务之间的同步取消信号,本质上是一种协程调度的方式。
context
出现意义context
实现原理context
注意事项context
总结
# 1. context 出现意义
在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。
我们可以使用done channel
来处理此类问题。比如以下这个例子:
func main() {
messages := make(chan int, 10)
done := make(chan bool)
defer close(messages)
// consumer
go func() {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-done:
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}()
// producer
for i := 0; i < 10; i++ {
messages <- i
}
time.Sleep(5 * time.Second)
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
上述例子中定义了一个buffer
为0 的``channel done
,子协程运行着定时任务。如果主协程需要在某个时刻发送消息通知子协程中断任务退出,那么就可以让子协程监听这个 done channel
,一旦主协程关闭 done channel
,那么子协程就可以推出了,这样就实现了主协程通知子协程的需求。
这很好,但是这也是有限的。
如果我们可以在简单的通知上附加传递额外的信息来控制取消:为什么取消,或者有一个它必须要完成的最终期限,更或者有多个取消选项,我们需要根据额外的信息来判断选择执行哪个取消选项。
考虑下面这种情况:假如主协程中有多个任务 1, 2, …m,主协程对这些任务有超时控制;而其中任务 1 又有多个子任务 1, 2, …n,任务 1 对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务 1 的取消信号。
如果还是使用 done channel
的用法,我们需要定义两个 done channel
,子任务们需要同时监听这两个 done channel
。这样其实好像也还行哈。但是如果层级更深,如果这些子任务还有子任务,那么使用 done channel
的方式将会变得非常繁琐且混乱。
我们需要一种优雅的方案来实现这样一种机制:
- 上层任务取消后,所有的下层任务都会被取消;
- 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。
这个时候 context
就派上用场了。
# 2. context 实现原理
# 2.1 context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
这里建议读一读 Go 源码上的注释,解释得非常详细。
context
接口包含四个方法:
Deadline()
: 返回绑定当前context
的任务被取消的截止时间,如果没有设定期限,将返回 ok == falseDone()
: 当当前的context
被取消时,将返回一个关闭的channel
,如果当前context
不会被取消,将返回 nilErr()
:- 如果
Done()
返回的channel
没有关闭,将返回nil
; - 如果
Done()
返回的channel
已经关闭,将返回非空的值表示任务结束的原因; - 如果是
context
被取消,Err()
将返回Canceled
, - 如果是
context
超时,Err()
将返回DeadlineExceeded
- 如果
Value()
: 返回context
存储的键值对中当前key
对应的值,如果没有对应的key
,则返回 nil
可以看到 Done()
方法返回的 channel
正是用来传递结束信号以抢占并中断当前任务;Deadline()
方法指示一段时间后当前 goroutine
是否会被取消;以及一个Err()
方法,来解释 goroutine
被取消的原因;而 Value()
则用于获取特定于当前任务树的额外信息。
而 context
所包含的额外信息键值对是如何存储的呢?其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到 key
所对应的值,就会向上去父节点里找,直到根节点,具体后面会说到。
再来看看 context
包中的其他关键内容。
# 2.2 emptyCtx
emptyCtx
是一个 int
类型的变量,但实现了 context
的接口。emptyCtx
没有超时时间,不能取消,也不能存储任何额外信息,所以 emptyCtx
用来作为 context
树的根节点。
// 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"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
Background
和 TODO
并没有什么不同,只不过用不同的名字来区分不同的场景罢了。
Background
通常被用于主函数、初始化以及测试中,作为一个顶层的 `context``TODO
是在不确定使用什么context
或者不知道有没有必要使用context
的时候才会使用
# 2.3 context.WithValue
valueCtx
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
valueCtx
利用一个 Context
类型的变量来表示父节点 context
,所以当前 context
继承了父 context
的所有信息;
valueCtx
类型还携带一组键值对,也就是说这种 context
可以携带额外的信息。valueCtx
实现了 Value
方法,用以在 context
链路上获取 key
对应的值,如果当前 context
上不存在需要的 key
,会沿着 context
链向上寻找 key
对应的值,直到根节点。
WithValue
WithValue
用以向 context
添加键值对:
- WithValue 返回父级的副本,其中与键关联的值为 val。
- 用于传输进程和 API 的请求范围的数据,而不用于将可选参数传递给函数。
key
必须具有可比性(comparable),并且不应是string
类型或任何其他内置类型,以避免在不同的package
之间使用context
包发生冲突。- 用户应为
key
定义自己的类型。为了避免在分配给 interface {} 时进行分配,key
通常具有具体的struct{}
。或者,导出的key
变量的静态类型应为指针或接口。
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
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}
}
这里添加键值对不是在原 context
结构体上直接添加,而是以此 context
作为父节点,重新创建一个新的 valueCtx
子节点,将键值对添加在子节点上,由此形成一条 context
链。获取 value
的过程就是在这条 context
链上由尾部向前搜寻:
# 2.4 context.WithCancel
cancelCtx
跟 valueCtx
类似,cancelCtx
中也有一个 context
变量作为父节点,其他属性:
- 变量
done
表示一个channel
,用来表示传递关闭信号; children
表示一个map
,存储了当前context
节点下的子节点;err
用于存储错误信息表示任务结束的原因。
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
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
}
下面来看最重要的 cancel()
方法:
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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
}
// 设置本次取消的原因
// 1. 设置一个关闭的 channel
// 2. 将 done channel 关闭
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 依次取消子 context
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 {
// 将当前 context 从父节点上移除
removeChild(c.Context, c)
}
}
cancelCtx
类型的 context
在调用 cancel()
方法时会设置取消原因,将 done channel
设置为一个关闭 channel
或者关闭 done channel
,然后将子节点 context
依次取消,如果有需要还会将当前节点从父节点上移除。
WithCancel
WithCancel
函数用来创建一个可取消的 context
,即 cancelCtx
类型的context
。WithCancel
返回一个 context
和一个 CancelFunc
,调用 CancelFunc
即可触发 cancel
操作。直接看源码:
type CancelFunc func()
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
// 1. 如果 `parent.Done()` 返回 `nil`,
// 表明父节点以上的路径上没有可取消的 `context`,
// 不需要处理;
if done == nil {
return // parent is never canceled
}
// 2. 如果父节点可取消且取消的话,就取消当前节点
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 3. 如果父节点可取消但是没有取消的话
// 判断父节点的父节点有没有取消(也就是判断祖先节点有没有取消)
// 如果取消的话,那也取消当前节点
// 如果没有取消的话,就把当前节点加入到祖先节点的 children 列表中
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 {
// 4. 如果祖先节点也不能取消的话,就开一个协程,监督 parent.Done() 和 child.Done()
// 重点是监督 parent.Done(),一旦它返回的 channel 被取消了,
// 即 context 链中某个祖先节点被取消,则将当前 context 也取消
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
// parentCancelCtx returns the underlying *cancelCtx for parent.
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
}
p.mu.Lock()
ok = p.done == done
p.mu.Unlock()
if !ok {
return nil, false
}
return p, true
}
之前说到 cancelCtx
取消时,会将后代节点中所有的 cancelCtx
都取消,propagateCancel
即用来建立当前节点与祖先节点这个取消关联逻辑:
- 如果
parent.Done()
返回nil
,表明父节点以上的路径上没有可取消的context
,不需要处理; - 如果在
context
链上找到到cancelCtx
类型的祖先节点,则判断这个祖先节点是否已经取消:- 如果已经取消就取消当前节点;
- 如果没有取消,就尝试寻找可取消的祖先节点:
- 找到可取消的祖先节点,判断祖先节点有没有取消:
- 祖先节点取消了,则取消当前节点;
- 祖先节点没有取消,将当前节点加入到祖先节点的
children
列表;
- 没找到可取消的祖先节点,则开一个新的协程监听
parent.Done()
和child.Done()
,重点是监听parent.Done()
,一旦它返回的channel
被取消了,即context
链中某个祖先节点被取消,则将当前context
也取消。
- 找到可取消的祖先节点,判断祖先节点有没有取消:
为什么要加入祖先节点的 children 列表,而不是加入父节点?
因为父节点未必是可取消的节点,如 backgroudContext -> cancelCtx -> valueCtx(parent) -> cancelCtx(current)
,这个时候父节点是不具备 cancelCtx
的,它没有 children
列表。
# 2.5 context.WithDeadline
timerCtx
timerCtx
是一种基于 cancelCtx
的 context
类型,从字面上就能看出,这是一种可以定时取消的 context
。
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 取消内部 cancelCtx
c.cancelCtx.cancel(false, err)
// 需要则从父节点的 children 列表中移除
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
内部使用 cancelCtx
实现 cancel()
,另外使用定时器 timer
和过期时间 deadline
实现定时取消的功能。timerCtx
在调用cancel()
方法,会先将内部的 cancelCtx()
取消,如果需要则将自己从 cancelCtx
祖先节点上移除,最后取消计时器。
WithDeadline
WithDeadline
返回一个基于 parent
的可取消的 context
,并且其过期时间 deadline
不晚于所设置时间 d
。
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// 简单是否有更早的 deadline,如果有则用更早的
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 建议新建 context 与可取消的 context 祖先节点的取消关联关系
propagateCancel(parent, c)
// 检查 deadline 是否已经过去了(相比于 d)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
// 设置定时器,过期则取消当前 timerCtx
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
- 如果父节点
parent
有过期时间并且过期时间早于给定时间d
,那么新建的子节点context
无需设置过期时间,使用WithCancel
创建一个可取消的context
即可; - 否则,就要利用
parent
和过期时间d
创建一个定时取消的timerCtx
,并建立新建context
与可取消context
祖先节点的取消关联关系,接下来判断当前时间距离过期时间d
的时长dur
: - 如果
dur
小于0,即当前已经过了过期时间,则直接取消新建的timerCtx
,原因为DeadlineExceeded
; - 否则,为新建的
timerCtx
设置定时器,一旦到达过期时间即取消当前timerCtx
。
# 2.6 context.WithTimeout
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
与 WithDeadline
类似,WithTimeout
也是创建一个定时取消的 context
,只不过:
WithDeadline
是接收一个过期时间点WithTimeout
接收一个相对当前时间的过期时长timeout
# 3. context 注意事项
- 不要把
context
放在结构体中,要以参数的方式显示传递; - 以
context
作为参数的函数方法,应该把context
作为第一个参数; - 给一个函数方法传递
context
的时候,不要传递nil
,如果不知道传递什么,就使用context.TODO()
; context
的Value
相关方法应该传递请求域的必要数据,不应该用于传递可选参数;context
是线程安全的,可以放心的在多个 Goroutine 中传递。- 使用
context.WithValue
来传值的情况非常少,在真正使用传值的功能时我们也应该非常谨慎,不能将请求的所有参数都使用context
进行传递,这是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。
# 4. context 总结
context.TODO
: 不知道用什么 context 以及不知道需不需要用 context 的时候用context.Background
: 一般用于根 contextcontext.WithValue
传值context.WithCancel
可取消context.WithDeadline
到指定时间点自动取消(或在这之前手动取消)context.WithTimeout
一段时间后自动取消(或在这之前手动取消)
Go 语言中的 context 的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费。