GO实验(5)context包阅读

环境

go1.18
阿里云centos7

context结构体

context.Context是一个需要实现4个方法的接口

type Context interface {
//返回被取消时的时间,即工作完成的截止时间
Deadline() (deadlin time.Time,ok bool)
//这个channel会在当前工作完成或上下文取消后被关闭,多次调用返回同一个channel
Done() <-chan struct{}
//context结束的原因,只会在done方法对应的channel关闭时返回非空值
//1.取消返回canceled错误
//2.超时返回deadlinExceeded错误
Err() error
//从上下文中获取对应键的值,对于同一个上下文,多次调用Value
//并传入相同的key会返回相同的结果
//该方法可以用来传递请求特定的数据
Value(key any) any
}

下面是两个错误信息在代码包中的定义

var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

Background和TODO

context最常用的两个方法是Background和TODO,这两个方法都会返回预先初始化好的私有变量

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

var (
        background = new(emptyCtx)
        todo       = new(emptyCtx)
)
//初始化时4个方法都返回nil
type emptyCtx int

两个方法都通过new方法初始化来获得emptyCtx的指针,也就是说这两个方法互为别名

4个衍生子上下文的函数

字根和父根之间的关系是一颗树

  • WithCancel
  • WithTimeout
  • WithDeadLine
  • WithValue

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
//var Canceled = errors.New("context canceled")
        return &c, func() { c.cancel(true, Canceled) }
}

传入父上下文,并通过newCancelCtx封装成cancelCtx结构体
context.propagateCancel 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消

func newCancelCtx(parent Context) cancelCtx {
        return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
        done := parent.Done()
//如果没有channel说明父上下文早就被关闭或无法关闭
        if done == nil {
                return // parent is never canceled
        }

        select {
        case <-done:
                // parent is already canceled
//cancel方法是会递归关闭子节点的done方法产生的channel
//因为父亲关闭了,子也必须关闭
//什么时候关闭?就是父亲的被关闭即 <-done
                child.cancel(false, parent.Err())
                return
        default:
        }

        if p, ok := parentCancelCtx(parent); ok {
                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 {
                atomic.AddInt32(&goroutines, +1)
                go func() {
                        select {
                        case <-parent.Done():
                                child.cancel(false, parent.Err())
                        case <-child.Done():
                        }
                }()
        }
}
  • 当parent.Done() == nil 时,即parent已经关闭或不会触发取消事件,则直接返回
  • 当parent取消时 <-done,child也会被取消
    如果没取消,则加入到parent的列表中等待parent释放取消信号
p.children[child] = struct{}{}
  • parentCancelCtx会尝试类型检测,当父上下文是开发者自定义类型,实现了Context接口并返回非空管道时,会运行一个新的Gorutine同时监听父与子的Done()方法的两个channel,并且父关闭时子也关闭
                go func() {
                        select {
                        case <-parent.Done():
                                child.cancel(false, parent.Err())
                        case <-child.Done():
                        }
                }()

propagteCancel保证了父被取消时,子也会收到对应信号,确保两者状态一致
传入progateCanel的child是newCancelCtx生成的cancelCtx结构体,对应的接口是canceler

type cancelCtx struct {
        Context
        mu       sync.Mutex            // protects following fields
        done     atomic.Value          // of 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
}


type canceler interface {
        cancel(removeFromParent bool, err error)
        Done() <-chan struct{}
}

看一下cancel方法的实现,removeFromParent参数 会影响后续removeChild函数的调用,决定是否从父节点的children map中删除

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
//继承父节点的错误信息
        if err == nil {
                panic("context: internal error: missing cancel error")
        }
        c.mu.Lock()
        if c.err != nil {
                c.mu.Unlock()
                return // already canceled
        }
        c.err = err
//实际是atomic.Value.Load()
// 尝试进行类型断言,确保父关闭通道
        d, _ := c.done.Load().(chan struct{})
        if d == nil {
                c.done.Store(closedchan)
        } else {
                close(d)
        }
//递归执行子节点的cancel,将子节点转为nil释放
        for child := range c.children {
                // NOTE: acquiring the child's lock while holding parent's lock.
                child.cancel(false, err)
        }
        c.children = nil
        c.mu.Unlock()
//删除cancelCtx.children中的c
        if removeFromParent {
                removeChild(c.Context, c)
        }
}

cancel方法关闭上下文中的channel并向所有子上下文同步取消信号,释放子上下文c.child = nil

WithTimeout和WithDeadLine

Timeout实际是对DeadLine的封装

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
//当前时间+过期时间 = 目标时间
        return WithDeadline(parent, time.Now().Add(timeout))
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
//判断父亲是否已经超时
        if cur, ok := parent.Deadline(); ok && cur.Before(d) {
                // The current deadline is already sooner than the new one.
                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 {
                c.timer = time.AfterFunc(dur, func() {
                        c.cancel(true, DeadlineExceeded)
                })
        }
        return c, func() { c.cancel(true, Canceled) }
}

WithDealine在创建timerCtx过程中会判断父的截至日期和当前日期,并通过time.AfterFunc创建定时器,超时则执行同步取消

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}
//不仅调用了cancelCtx的cancel方法,还停止定时器
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()
}

WithValue

func WithValue(parent Context, key, val any) Context {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        if key == nil {
                panic("nil key")
        }
        if !reflectlite.TypeOf(key).Comparable() {
                panic("key is not comparable")
        }
        return &valueCtx{parent, key, val}
}

type valueCtx struct {
        Context
        key, val any
}

创建一个valueCtx用于传值

func (c *valueCtx) Value(key any) any {
        if c.key == key {
                return c.val
        }
        return value(c.Context, key)
}

func value(c Context, key any) any {
        for {
                switch ctx := c.(type) {
                case *valueCtx:
                        if key == ctx.key {
                                return ctx.val
                        }
                        c = ctx.Context
                case *cancelCtx:
                        if key == &cancelCtxKey {
                                return c
                        }
                        c = ctx.Context
                case *timerCtx:
                        if key == &cancelCtxKey {
                                return &ctx.cancelCtx
                        }
                        c = ctx.Context
                case *emptyCtx:
                        return nil
                default:
                        return c.Value(key)
                }
        }
}

如果键值对匹配则直接从当前返回,找不到就递归从父节点找,直到找到或返回nil(父节点存储在Context字段)

总结

context
  • 创建父节点的办法是Background和Todo,两者刚创建的是一个空的父节点
  • 父节点派生出子节点的方法是WithCancel/Value/Timeout/Deadline
    子节点和父节点(父节点记录在Context中)的关系是以链表形式,父节点和子节点关系是树(子节点在children map中记录)
  • withCancel生成cancelCtx结构体,是canceler接口的实现,实现了cancel方法(取消原理是内部监听父Done方法,进行同步取消child=nil)
  • withTimeout是withDeadline的封装,生成timerCtx结构体(内嵌cancelCtx,所以也可以使用取消方法)
  • withvalue生成valueCtx结构体(没有cancel方法),寻找键值对会一直顺着父根网上(并不会遍历同级的)直到找到或返回nil。在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 传递请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

参考

1.上下文Context

  1. atomic
    3.context源码阅读

你可能感兴趣的:(GO实验(5)context包阅读)