[Go]context上下文--使用要点--源码分析--Go核心--并发编程

参考:
https://learnku.com/docs/go-interviews/7-context/16574

intro:
Go最核心的就是routine并发编程。但是routine也具有一定的Metadata,并且执行会占用调度时间和CPU时间
在开过多routine后,可能会因为内存爆掉直接导致项目崩溃。
这时候就需要对自己所使用的routine进行管理。

由于routine按照树状模式展开,context本身的存在就是为了routine而存在的。所以context在使用过程中也会呈现出树状结构

这里先直接PO出Go 开发者的建议:

  1. 不要把context放在结构体里面,直接作为函数调用的第一个参数传入
  2. 不要传入nil,不知道传什么传递nil
  3. 不要把普通的函数参数放到context进行保存,context应该存储共同的数据,比如cookie等
  4. 同一个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主要作为根context
  • TODO只是在不知道用什么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)
    }
}

其中最主要的事情:

  1. 判断是否被别的已经cancel了
  2. 遍历所有children,全部cancel
  3. 断绝与所有children的关系
  4. 根据字段判断是否需要断绝父子关系

而在中间遍历的child.cancel也解释了为什么这个方法需要一个字段评判。

  • 如果是父提出的cancel,那么就不需要断绝关系,因为本身父就需要将childrennil
  • 如果是孩子提出的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) }
}

需要注意的:

  1. time类别本身也是一个cancel
  2. 但imer也可以以非cancel作为父,因为调用的是newCalcelCtx(parent)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值