本文对golang context的源码进行解读,go version is go1.16.4 linux/amd64
context
我们可能会创建多个 Goroutine 来协同处理一个请求,而 context.Context
的作用是在不同 Goroutine 之间同步请求的特定数据、取消信号以及处理请求的截止日期(request data, cancel signal and deadline)
即,【goroutine with context】
两个interface: context.Context & context.canceler
- context.Context
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回 context.Context 被取消的时间,也就是完成工作的截止时间
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{} // 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
Err() error //
Value(key interface{}) interface{}
}
Deadline
— 返回context.Context
被取消的时间,也就是完成工作的截止时间;-
Done
— 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用Done
方法会返回同一个 Channel;context取消->Channel关闭->监听的goroutine读Channel读出零值->得知context取消->goroutine退出;(从已关闭的 channel 接收数据,返回已缓冲数据或零值)Done()
返回的 channel本质上就是一个二值的开关:当这个 channel 被关闭时,说明 context 被取消了。注意,返回的是一个只读的channel。 我们知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个receive-only
的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出:- 读不出值,context没取消
- 读出值,context被取消
所有相关goroutine监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到
-
Err
— 返回context.Context
结束的原因,它只会在Done
方法对应的 Channel 关闭时返回非空的值;- 如果
context.Context
被取消,会返回Canceled
错误; - 如果
context.Context
超时,会返回DeadlineExceeded
错误;
- 如果
Value
— 从context.Context
中获取键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的Key
会返回相同的结果,该方法可以用来传递请求特定的数据;
- context.canceler
type canceler interface {
cancel(removeFromParent bool, err error) // removeFromParent 表示是否主动断绝与父Context的关系
Done() <-chan struct{}
}
对于实现了Context接口的数据类型,再实现一个cancel(removeFromParent bool, err error)
方法,就变成了可取消的Context
为什么canceler要实现cancel和Done两个方法呢?
首先,和Context类似,多个goroutine都可以监听canceler.Done()返回的只读channel,当canceler发生取消操作时,每个监听者都将收到消息
-
cancel()
方法的功能就是关闭自己的 channel:close(c.done);然后loop取消它的所有子Context(调用child.cancel(false, err)
,并且主动断绝和子Context的关系c.children = nil
);最后让父Context在children中删除自己父子Context切断联系,本质上就是从p.children中删除child
源码中有两个类型实现了 canceler 接口:*cancelCtx
和 *timerCtx
2个主要的cancel结构体
-
context.cancelCtx
.context.WithCancel
函数返回的结构体。
// 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
}
实际上,一个 cancelCtx c 的
Context
记录了c的parent,children
记录了c的child。这样一来,cancelCtx的上下游联系都被完整记录了
其实,从数据结构的视角看,这就是在最通用的tree结点的基础上,加入了与父结点的关系,从而完全打通了与上下的交互;同时,也有点双向链表的意思
// 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")
}
// 将parent作为父节点context生成一个新的子节点
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()
if done == nil {
return // parent is never canceled 父Context永远不会取消,直接return
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 下面这个if很重要
// 【尝试把父Context转成*cancelCtx p,如果可以转,就把child加到父Context(as a cancelCtx)的children中】
// 【这样只要父cancelCtx调用cancel,其所有child都会跟着被cancel】
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{})
}
// 把child加到父Context的children中
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 【如果父Context无法转换成*cancelCtx,那么只能直接开个goroutine监听parent.Done(),当parent取消时,紧接着child.cancel】
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
context.WithCancel
能从当前的 context.Context
中衍生出一个新的子上下文并返回用于取消该子上下文的cancel函数
context.WithCancel
将通过func propagateCancel(parent Context, child canceler)构建父子context的关联,父context取消时,子context也将被取消。通过一个个这样的父子链关系传递,可以取消整个context树。下面看下具体cancel()方法的实现:取消自己,取消children,根据需要断绝自己和父Context的关系
// 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
}
c.err = err
if c.done == nil {
c.done = closedchan // var closedchan = make(chan struct{}), closedchan is a reusable closed channel.
} else {
close(c.done)
}
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 {
removeChild(c.Context, c) // c.Context refers to c's parent
}
}
-
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu deadline time.Time }
context.timerCtx
内部不仅通过嵌入context.cancelCtx
结构体继承了相关的变量和方法,还通过持有的定时器timer
和截止时间deadline
实现了定时取消的功能
*context.timerCtx
的cancel调用了*cancelCtx.cancel(removeFromParent bool, err error)
(其实也没那么简单,见下)
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()
}
这里有一点需要思考:
为什么调用c.cancelCtx.cancel
时removeFromParent直接指定为false呢?直接写成下面的样子不可以吗?
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(removeFromParent, err)
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
这其实是context的父子关系决定的。需要搞清楚的是,c.cancelCtx.Context是c的父context,而非c.cancelCtx的父context,这是WithDeadline创建timerCtx的时候确定的:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
...
// 创建了一个timerCtx并把它与parent建立父子关系
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c) // not propagateCancel(parent, c.cancelCtx)
...
}
因此,直接
c.cancelCtx.cancel(removeFromParent, err)
并不能解除c与c.cancelCtx.Context的关系,而是取消了c.cancelCtx与c.cancelCtx.Context的关系
context.WithDeadline
在创建 context.timerCtx
的过程中还判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc
创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel
同步取消信号
Go 语言中的 context.Context
的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到