参考:
https://learnku.com/docs/go-interviews/7-context/16574
intro:
Go最核心的就是routine并发编程
。但是routine也具有一定的Metadata
,并且执行会占用调度时间和CPU时间。
在开过多routine后,可能会因为内存爆掉直接导致项目崩溃。
这时候就需要对自己所使用的routine进行管理。
由于routine按照树状模式展开,context本身的存在就是为了routine而存在的。所以context在使用过程中也会呈现出树状结构。
这里先直接PO出Go 开发者的建议:
- 不要把context放在结构体里面,直接作为函数调用的第一个参数传入
- 不要传入nil,不知道传什么传递nil
- 不要把普通的函数参数放到context进行保存,context应该存储共同的数据,比如cookie等
- 同一个context可能会被传递到多个routine,但是context是并发安全的
源码开撕
第一个是context
type Context interface {// 当 context 被取消或者到了deadline,返回一个被关闭的 channelDone() <-chan struct{}// 在 channel Done 关闭后,返回 context 取消原因Err() error// 返回 context 是否会被取消以及自动取消时间(即 deadline)Deadline() (deadline time.Time, ok bool)// 获取 key 对应的 valueValue(key interface{}) interface{}
}
可以看到context本身是以接口方式存在的,这里就给了一定空间进行自定义。可以自己实现接口完成自己的context。
Done
返回一个chan,但是这个chan是一个只读的,所以一般只作为触发器,如果进行读入操作会导致当前routine挂起Err
当时间到了,或者关闭,保存关闭原因Deadline
表示这个context截止时间,可以根据这个时间,routine决定是否需要进行某些操作。Value
获取之前设置key 的value
接下来是源码自带的emptyContext
:
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
}var (background = new(emptyCtx)todo = new(emptyCtx)
)
func Background() Context {return background
}func TODO() Context {return todo
}
都实现了接口但都是朴实无华的nil
并且将这个empty起了两个名字,一个叫做background
,一个叫做todo
background
主要作为根contextTODO
只是在不知道用什么context时使用,作为一个代码标注而已
接下来到了最重要的context:cancelContext
首先先两出两个cancel context的理念:
- caller不应该过度干涉callee的情况,决定如何以及何时return应该由callee来决定。caller只能够给出建议需要关闭。
- 取消操作可以进行树状传递
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}type cancelCtx struct {Context// 保护之后的字段mu sync.Mutexdone chan struct{}children map[canceler]struct{}err error
}
func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}
其中第一个声明了canceler接口,只要具有Done和cancel函数就表示这是一个可以cancel的接口。换言之只要实现cancel(removeFromParent bool, err error)
的context就一定是一个可cancel的context。因为context本身就需要实现Done
。只是说提醒一下需要重写Done
函数。
这两个函数大小写就印证了第一个理念,routine只能够Done表示这个context应该结束,而没有权利直接cancel掉,按照链式(在后面)也没有权利调用子context,只能够传递我认为应该需要cancel掉的信息。
同时也进行加锁操作,保证了并发的安全性。
children
字段印证了第二个理念,cancel需要传递。
再来看看cancel函数
func (c *cancelCtx) cancel(removeFromParent bool, err error) {// 必须要传 errif err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // 已经被其他协程取消}// 给 err 字段赋值c.err = err// 关闭 channel,通知其他协程if c.done == nil {c.done = closedchan} else {close(c.done)}// 遍历它的所有子节点for child := range c.children {// 递归地取消所有子节点child.cancel(false, err)}// 将子节点置空c.children = nilc.mu.Unlock()if removeFromParent {// 从父节点中移除自己 removeChild(c.Context, c)}
}
其中最主要的事情:
- 判断是否被别的已经cancel了
- 遍历所有children,全部cancel
- 断绝与所有children的关系
- 根据字段判断是否需要断绝父子关系
而在中间遍历的child.cancel
也解释了为什么这个方法需要一个字段评判。
- 如果是父提出的
cancel
,那么就不需要断绝关系,因为本身父就需要将children
置nil
- 如果是孩子提出的
cancel
,这时候就需要断绝父子关系(如下面这个)
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}
}
var Canceled = errors.New("context canceled")
最后一个重要的函数
propagateCancel
func propagateCancel(parent Context, child canceler) {// 父节点是个空节点if parent.Done() == nil {return // parent is never canceled}// 找到可以取消的父 contextif p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// 父节点已经被取消了,本节点(子节点)也要取消child.cancel(false, p.err)} else {// 父节点未取消if p.children == nil {p.children = make(map[canceler]struct{})}// "挂到"父节点上p.children[child] = struct{}{}}p.mu.Unlock()} else {// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {for {switch c := parent.(type) {case *cancelCtx:return c, truecase *timerCtx:return &c.cancelCtx, truecase *valueCtx:parent = c.Contextdefault:return nil, false}}
}
可以看到上面生成的cancel还有一个问题是没有绑定父子关系,这个操作就由propagateCancel
函数实现。
而最有意思的就是这个函数的最后几行,他有另外一个分支,假设说父routine不可取消(其实也就不是cancel类,只是empty或者是value类),那么就会开辟一个routine去监听,父与子的Done信号。
为什么需要这么做?
因为可能存在父是cancel,但是子并不是cancel,而子子又是cancel。
比如说父是cancel类别,子是KV context。这时候cancel里面的child不会保存子KV context,而子context会继承父cancel,此时这个子KV context又创建了一个cancel context。
这时候假设父需要cancel,父不会调用子子cancel方法,因为父cancel里面的children没有这个子子cancel。
但这个子子cancel会知道父cancel的Done信息,因为KV context是直接继承了父cancel,所以KV context的Done与父cancel的Done是同一个。
所以需要这个routine,来保证不会出现上述这个边界情况。
那么为什么有需要监听自己的Done呢?
因为如果自身都已经cancel了,就没必要去关心爷爷是否cancel了。跑这个routine只会占用系统资源。
以下是KV context,唯一一个需要注意的是,KV里面存储的不是线程安全的。所以一般只存放只读信息。
type valueCtx struct {Contextkey, val interface{}
}
func (c *valueCtx) String() string {return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}
func WithValue(parent Context, key, val interface{}) Context {if key == nil {panic("nil key")}if !reflect.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
最后一个就是timer
type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {// 直接调用 cancelCtx 的取消方法c.cancelCtx.cancel(false, err)if removeFromParent {// 从父节点中删除子节点removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {// 关掉定时器,这样,在deadline 到来时,不会再次取消c.timer.Stop()c.timer = nil}c.mu.Unlock()
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数return WithCancel(parent)}// 构建 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline: deadline,}// 挂靠到父节点上propagateCancel(parent, c)// 计算当前距离 deadline 的时间d := time.Until(deadline)if d <= 0 {// 直接取消c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(true, Canceled) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {// d 时间后,timer 会自动调用 cancel 函数。自动取消c.timer = time.AfterFunc(d, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
需要注意的:
- time类别本身也是一个cancel
- 但imer也可以以非cancel作为父,因为调用的是
newCalcelCtx(parent)