九. Go并发编程--context.Context (2)

一般用于创建 root context,这个 context 永远也不会被取消,或超时
TODO(), 底层和 Background 一致,但是含义不同,当不清楚用什么的时候或者是还没准备好的时候可以用它

var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }

查看源码我们可以发现,background 和 todo 都是实例化了一个 emptyCtx. emptyCtx又是一个 int类型的别名

type emptyCtx int // emptyCtx分别绑定了四个方法,而这四个方法正是 context接口定义的方法,所以emptyCtx实现了 Context接口 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 }

emptyCtx 就如同他的名字一样,全都返回空值

2.2.3 WithCancel

WithCancel(), 方法会创建一个可以取消的 context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } // 包装出新的 cancelContext c := newCancelCtx(parent) // 构建父子上下文的联系,确保当父 Context 取消的时候,子 Context 也会被取消 propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }

传入的参数是一个实现了Context接口的类型(root context),且不能为 nil, 所以我们经常使用方式如下:

// context.Background 返回的是一个 root context context.WithCancel(context.Background())

不止 WithCancel 方法,其他的 WithXXX 方法也不允许传入一个 nil 值的父 context
newCancelCtx 只是一个简单的包装就不展开了, propagateCancel 比较有意思,我们一起来看看

func propagateCancel(parent Context, child canceler) { // 首先判断 parent 能不能被取消 done := parent.Done() if done == nil { return // parent is never canceled } // 如果可以,看一下 parent 是不是已经被取消了,已经被取消的情况下直接取消 子 context select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } // 这里是向上查找可以被取消的 parent context if p, ok := parentCancelCtx(parent); ok { // 如果找到了并且没有被取消的话就把这个子 context 挂载到这个 parent context 上 // 这样只要 parent context 取消了子 context 也会跟着被取消 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 { // 如果没有找到的话就会启动一个 goroutine 去监听 parent context 的取消 channel // 收到取消信号之后再去调用 子 context 的 cancel 方法 go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }

接下来我们就看看 cancelCtx 长啥样

type cancelCtx struct { Context // 这里保存的是父 Context mu sync.Mutex // 互斥锁 done chan struct{} // 关闭信号 children map[canceler]struct{} // 保存所有的子 context,当取消的时候会被设置为 nil err error }

Done()

在 Done 方法这里采用了 懒汉式加载的方式,第一次调用的时候才会去创建这个 channel

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 }

Value()

value方法很有意思,这里相当于是内部 cancelCtxKey 这个变量的地址作为了一个特殊的 key,当查询这个 key 的时候就会返回当前 context 如果不是这个 key 就会向上递归的去调用 parent context 的 Value 方法查找有没有对应的值

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

在前面讲到构建父子上下文之间的关系的时候,有一个去查找可以被取消的父 context 的方法 parentCancelCtx 就用到了这个特殊 value

func parentCancelCtx(parent Context) (*cancelCtx, bool) { // 这里先判断传入的 parent 是否是一个有效的 chan,如果不是是就直接返回了 done := parent.Done() if done == closedchan || done == nil { return nil, false } // 这里利用了 context.Value 不断向上查询值的特点,只要出现第一个可以取消的 context 的时候就会返回 // 如果没有的话,这时候 ok 就会等于 false p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } // 这里去判断返回的 parent 的 channel 和传入的 parent 是不是同一个,是的话就返回这个 parent p.mu.Lock() ok = p.done == done p.mu.Unlock() if !ok { return nil, false } return p, true }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgzjys.html