参考:
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,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
可以看到context本身是以接口方式存在的,这里就给了一定空间进行自定义。可以自己实现接口完成自己的context。
Done
返回一个chan,但是这个chan是一个只读的,所以一般只作为触发器,如果进行读入操作会导致当前routine挂起Err
当时间到了,或者关闭,保存关闭原因Deadline
表示这个context截止时间,可以根据这个时间,routine决定是否需要进行某些操作。Value
获取之前设置key 的value
接下来是源码自带的emptyContext
:
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
}
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.Mutex
done 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.done
c.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) {
// 必须要传 err
if 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 = nil
c.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
}
// 找到可以取消的父 context
if 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, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
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 {
Context
key, 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 {
cancelCtx
timer *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)
}
// 构建 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// 挂靠到父节点上
propagateCancel(parent, c)
// 计算当前距离 deadline 的时间
d := time.Until(deadline)
if d <= 0 {
// 直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return 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)