应用场景
主要在goroutine之间传递上下文信息,包括:退出信号、超时与截止时间、k-v键值对等。
context的产生
goroutine本身并没有parent/children关系,当遇到复杂的并发结构时,处理下游goroutine的退出将会变得艰难。
比如一个网络请求Request,每一个请求都需要开启一个或者多个goroutine处理一些业务,这些goroutine处理的过程中又衍生了更多的goroutine,goroutine的关系链把场景变的非常复杂。
在Go1.7版本开始提供了context标准库来解决类似问题。
context底层实现
整体类图
interface
context接口
作为一个基本接口,所有的context对象都要实现该接口。内部定义了4个方法(都是幂等)。
type Context interface { // 返回取消该上下文完成的工作的时间。 // 如果未设置截止日期,则截止日期返回ok == false。 // 连续调用Deadline会返回相同的结果。 Deadline() (deadline time.Time, ok bool) // 在context被取消或者到了deadline,返回一个被关闭的channel。 // 在源码中,因为没有任何地方向这个channel里面写入值,而且这是一个只读 // 的channel,因此,在goroutine中监听这个channel的时候,只有被关 // 闭的时候,才会读取到相应类型的零值,后续子goroutine便可以做退出操作。 // func Stream(ctx context.Context, out chan // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case // return ctx.Err() // case out // } // } // } Done() struct{} // 如果尚未关闭Done,则Err返回nil。 // 如果关闭了Done,Err将返回一个被关闭的原因: // 1、如果context已取消,返回context canceled; // 2、如果context的截止日期已过,返回context deadline exceeded。 Err() error // 获取key对应的value Value(key interface{}) interface{}}
canceler接口
为拓展接口。实现了此接口的context,说明该context是可取消的。
// A canceler is a context type that can be canceled directly. The// implementations are *cancelCtx and *timerCtx.type canceler interface { cancel(removeFromParent bool, err error) Done() chan }
struct
emptyCtx
emptyCtx实现了一个不具备任何功能的context接口,也就是一个空的contex。emptyCtx永远不会被cancel,它没有值,也没有 deadline。
它不是struct {},因为此类型的var必须具有不同的地址。它主要是作为context对象树的root节点。
type emptyCtx intfunc (*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}// todo和background两者本质上只有名字区别,在按string输出的时候会有区别。// emptyCtx的String方法可以返回当前context的具体类型,比如是Background还是TODO。// background和todo是两个全局变量,这里通过取其地址来进行对应类型的判断。func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context"}
emptyCtx它被包装成全局变量,分别通过context.Background()和context.TODO()两个函数对用户公开。
var ( background = new(emptyCtx) todo = new(emptyCtx))// Background返回一个非空的Context。// 它通常由main函数,初始化和测试使用,并作为所有context的根节点。func Background() Context { return background}// TODO返回一个非空的Context。TODO通常用在并不知道传递什么context的情形,代码应使用context.TODO。// 在很多的函数调用的过程中都会传递但是通常又不会使用,比如既不会监听退出,也不会从其中获取数据。// TODO跟Background一样,也是返回一个全局变量。func TODO() Context { return todo}
Background
cancelCtx
cancelCtx结构体内嵌了一个Context对象,即其parent context,这样,它就可以被看成一个Context。
同时内部还通过children来保存所有可以被取消的context的接口,后续如果当前context被取消的时候,只需要调用所有canceler接口的context就可以实现当前调用链的取消。
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}
如果当前的goroutine持有的Context实例是可被Cancel的,那么它的所有child goroutine也应当是可被Cancel的,这也是cancelCtx类中存在Context字段和children字段的原因。
cancelCtx结构体字段解释:Context为父context;done字段是Done()方法的返回值;children中的key记录着所有的孩子,value是没有意义的;err字段是Err()方法的返回值。
Done方法的实现
func (c *cancelCtx) Done() struct{} { c.mu.Lock() // 内部变量 done “懒加载”,只有调用了 Done() 方法的时候才会被创建。 if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d}
Done操作返回当前的一个chan,用于通知goroutine退出。
cancel() 方法的实现
// 参数removeFromParent:是否需要把它从父节点的孩子中除名。// 将参数err赋值给字段err, 在方法Context.Err中返回。func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } // context一旦被某个操作操作触发取消后,就不会再进行任何状态的修改。 c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err // 关闭channel,通知其他协程。 if c.done == nil { c.done = closedchan } else { close(c.done) } // 递归调用所有children取消。 for child := range c.children { // 当c调用返回的cancelFunc时,执行cancel方法,会将c从它的父节点里移除(见[2])。 // 而c所有的children都会因为c.children = nil移除(见[1]),无需一个个处理。 child.cancel(false, err) } // [1] c.children = nil c.mu.Unlock() // 如果为true,从父节点中移除自己[2]。 if removeFromParent { removeChild(c.Context, c) }}// 全局复用的, 被关闭的channel。var closedchan = make(chan struct{})func init() { close(closedchan)}// parentCancelCtx判断Context实例是否是一个可被Cancel的类型。// parentCancelCtx只识别context标准包内的三种类型。// 如果用户自己的类实现了context.Context接口,或者把ctx包在了自己的类型内,或者是emptyCtx,将执行default。func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true case *timerCtx: return &c.cancelCtx, true case *valueCtx: parent = c.Context default: return nil, false } }}// removeChild removes a context from its parent.func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock()}
cancel关闭c.done,取消c的每个children,如果removeFromParent为true,则从其父级的子级中删除c。
WithCancel方法
它可以创建一个可取消的context。
// 多个goroutine可以同时调用CancelFunc,在第一个调用之后,随后对CancelFunc的调用什么都不做。// 调用该函数意味着将关闭Context, 结束相关的work。type CancelFunc func()// 从parent上派生出一个新的Context,并返回一个CancelFunc类型的函数。// 当调用对应的cancel函数时,该Context对应的Done()返回的只读channel也会被关闭。// 取消Context将释放与其关联的资源,因此在此context中某项任务的操作完成后,代码应立即调用返回的cancel函数。func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }} // 下面是WithCancel两个内部方法的实现。func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent}}func propagateCancel(parent Context, child canceler) { // 说明父节点是emptyCtx,或者用户自己实现的context.Context。 if parent.Done() == nil { return // parent is never canceled } // 如果父Context本身是可Cancel的。 if p, ok := parentCancelCtx(parent); ok { // 说明父Context是以下三种之一: // 1. 是一个cancelCtx, 本身就可被Cancel; // 2. 是一个timerCtx, timerCtx是canctx的一个子类, 也可被Cancel; // 3. 是一个valueCtx, valueCtx继承体系上的某个爹, 是以上两者之一。 p.mu.Lock() if p.err != nil { // parent has already been canceled // 父节点已经被取消了,本节点(子节点)也要取消。 child.cancel(false, p.err) } else { // 若父节点未取消, 就把子节点添加到它的children列表中去。 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // 说明父Context虽然可被Cancel,但并不是标准库中预设的cancelCtx或timerCtx两种可被Cancel的类型。 // 这意味着这个特殊的父Context, 内部并不能保证记录了所有儿子的列表。 // 新开一个goroutine, 时刻监视着父Context的生存状态。 go func() { select { // 当父Context取消, 就立即调用child.cancel处理子节点。 case Done(): child.cancel(false, parent.Err()) // 如果子节点自己取消了,那就退出这个select,父节点的取消信号就不用管了。 // 如果去掉这个case,那么很可能父节点一直不取消,这个goroutine就泄漏了。 // 如果父节点取消了,子节点就会重复取消。 case Done(): } }() }}
调用cancel方法的时候,第一个参数是true,在取消的时候需要将自己从父节点里删除。第二个参数Canceled是一个固定的取消错误类型。
// Canceled is the error returned by Context.Err when the context is canceled.var Canceled = errors.New("context canceled")
timerCtx
带有超时的contex。
timerCtx继承了cancelCtx接口,同时还包含一个timer.Timer定时器(由标准库的time.Timer实现)和一个deadline终止实现。Timer会在deadline到来时,自动取消context。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time}
Deadline()方法
它只是简单的字段deadline的getter。
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true}
cancel()取消方法
它重写了canceler.cancel方法。
cancel()方法首先进行cancelCtx的取消流程,然后进行自身的定时器的Stop操作。
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()}
WithDeadline()方法
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { // 如果父节点也是一个有dealine的Context,而且dealine更靠前,以父节点的为准。 // 一旦父节点超时,子节点也会随之取消。 return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) // 如果时间已经到达,直接取消。 dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { // d时间后,timer会自动调用cancel函数,当前的goroutine不会被阻塞。 c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
可以看到,timerCtx只是对cancelCtx在功能上的追加WithDeadline也只是简单的追加了一个定时器。
如果要创建的这个子节点的deadline比父节点要晚,子节点在deadline 到来之前就已经被父节点取消了。
timer调用cancel函数传入的参数如下。
var DeadlineExceeded error = deadlineExceededError{}type deadlineExceededError struct{}func (deadlineExceededError) Error() string { return "context deadline exceeded" }
另外,标准包还提供了一个WithTimeout函数,其实与WithDeadline是等价的。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}
WithTimeout函数调用了WithDeadline,传入的deadline是当前时间加上timeout的时间,需要用的是绝对时间。
valueCtx
带数据共享的非公开类valueCtx。其内部通过一个key/value进行值的存储,并且只能存储一个key(可比较的key)。
如果当前context不包含值则进行层层向上递归(父节点没法获取子节点存储的值,子节点却可以获取父节点的值)。
type valueCtx struct { Context key, val interface{}}func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val}}func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(type " + reflectlite.TypeOf(c.key).String() + ", val " + stringify(c.val) + ")"}func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
参考资料
[1] 深度解密Go语言之context:
https://qcrao.com/2019/06/12/dive-into-go-context
[2] Go context:
https://github.com/cch123/golang-notes/blob/master/context.md
[3] 图解Go语言的context了解编程语言核心实现源码:
https://www.cnblogs.com/buyicoding/p/12155169.html
[4] golang中的context包:
https://www.cnblogs.com/neooelric/p/10668820.html