«

Go中的Context怎么使用

时间:2024-6-16 06:33     作者:韩俊     分类: Go语言


这篇“Go中的Context怎么使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Go中的Context怎么使用”文章吧。

    1、Context定义

    Context 接口定义如下

    type Context interface {
      // Deadline returns the time when this Context will be canceled, if any.
        Deadline() (deadline time.Time, ok bool)
      // Done returns a channel that is closed when this Context is canceled
      // or times out.
        Done() <-chan struct{}
      // Err indicates why this context was canceled, after the Done channel
      // is closed.
        Err() error
      // Value returns the value associated with key or nil if none.
        Value(key any) any
    }

    Deadline()
    : 返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。

    Done()
    : 返回一个只读的通道(只有在被cancel后才会返回),类型为
    struct{}
    。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。这里就简称信号通道吧!

    Err()
    :返回Context 被取消的原因

    Value
    : 从Context中获取与Key关联的值,如果没有就返回nil

    2、Context的派生

    2.1、创建Context对象

    context
    包提供了四种方法来创建context对象,具体方法如下:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
    func WithValue(parent Context, key, val any) Context {}

    由以上方法可知:新的context对象都是基于父context对象衍生的.

      WithCancel
      :创建可以取消的Context

      WithDeadline
      : 创建带有截止时间的Context

      WithTimeout
      :创建带有超时时间的Context,底层调用的是
      WithDeadline
      方法

      WithValue
      :创建可以携带KV型数据的Context

    简单的树状关系如下(实际可衍生很多中):

    2.2、parent Context

    context
    包默认提供了两个根context 对象
    background
    todo
    ;看实现两者都是由emptyCtx创建的,两者的区别主要在语义上,

      context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;

      context.TODO 应该仅在不确定应该使用哪种上下文时使用

    var (
        background = new(emptyCtx)
        todo       = new(emptyCtx)
    )
    // Background 创建background context
    func Background() Context {
        return background
    }
    // TODO 创建todo context
    func TODO() Context {
        return todo
    }

    3、context 接口四种实现

    具体结构如下,我们大致看下相关结构体中包含的字段,具体字段的含义及作用将在下面分析中会提及。

      emptyCtx

    type emptyCtx int // 空context

      cancelCtx

    type cancelCtx struct {
        Context // 父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                 // cancel的原因
    }

      timerCtx

    type timerCtx struct {
        cancelCtx //父context
        timer *time.Timer // 定时器
        deadline time.Time // 截止时间
    }

      valueCtx

    type valueCtx struct {
        Context // 父context
      key, val any // kv键值对
    }

    4、 emptyCtx 源码分析

    emptyCtx
    实现非常简单,具体代码如下,我们简单看看就可以了

    // An emptyCtx is never canceled, has no values, and has no deadline. It is not
    // struct{}, since vars of this type must have distinct addresses.
    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 any) any {
        return nil
    }
    func (e *emptyCtx) String() string {
        switch e {
        case background:
            return "context.Background"
        case todo:
            return "context.TODO"
        }
        return "unknown empty Context"
    }

    5、 cancelCtx 源码分析

    cancelCtx
    的实现相对复杂点,比如下面要介绍的timeCtx 底层也依赖它,所以弄懂
    cancelCtx
    的工作原理就能很好的理解
    context
    .

    cancelCtx
    不仅实现了
    Context
    接口也实现了
    canceler
    接口

    5.1、对象创建withCancel()

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        if parent == nil { // 参数校验
            panic("cannot create context from nil parent")
        }
      // cancelCtx 初始化
        c := newCancelCtx(parent)
        propagateCancel(parent, &c) // cancelCtx 父子关系维护及传播取消信号
        return &c, func() { c.cancel(true, Canceled) } // 返回cancelCtx对象及cannel方法
    }
    // newCancelCtx returns an initialized cancelCtx.
    func newCancelCtx(parent Context) cancelCtx {
        return cancelCtx{Context: parent}
    }

    用户调用

    WithCancel
    方法,传入一个父 Context(这通常是一个
    background
    ,作为根节点),返回新建的 context,并通过闭包的形式返回了一个 cancel 方法。如果想要取消context时需手动调用cancel方法。

    5.1.1、newCancelCtx

    cancelCtx对象初始化, 其结构如下:

    type cancelCtx struct {
      // 父 context
        Context //  parent context
        // 锁 并发场景下保护cancelCtx结构中字段属性的设置
        mu       sync.Mutex            // protects following fields 
      // done里存储的是信号通道,其创建方式采用的是懒加载的方式
        done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call 
      // 记录与父子cancelCtx对象,
        children map[canceler]struct{} // set to nil by the first cancel call
      // 记录ctx被取消的原因
        err      error                 // set to non-nil by the first cancel call
    }

    5.1.2、propagateCancel

    propagateCancel

    // propagateCancel arranges for child to be canceled when parent is.
    func propagateCancel(parent Context, child canceler) {
        done := parent.Done() // 获取parent ctx的信号通道 done
        if done == nil {  // nil 代表 parent ctx 不是canelctx 类型,不会被取消,直接返回
            return // parent is never canceled
        }
        select { // parent ctx 是cancelCtx类型,判断其是否被取消
        case <-done:
            // parent is already canceled
            child.cancel(false, parent.Err())
            return
        default:
        }
      //parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的
        if p, ok := parentCancelCtx(parent); ok { // 查询到
            p.mu.Lock() // 加锁
            if p.err != nil { // 祖父 ctx 已经被取消了,则 子cancelCtx 也需要调用cancel 方法来取消
                // parent has already been canceled
                child.cancel(false, p.err)
            } else { // 使用map结构来维护 将child加入到祖父context中
                if p.children == nil {
                    p.children = make(map[canceler]struct{})
                }
                p.children[child] = struct{}{}
            }
            p.mu.Unlock()// 解锁
        } else { // 开启协程监听 parent Ctx的取消信号 来通知child ctx 取消
            atomic.AddInt32(&goroutines, +1)
            go func() {
                select {
                case <-parent.Done():
                    child.cancel(false, parent.Err())
                case <-child.Done():
                }
            }()
        }
    }
    // parentCancelCtx returns the underlying *cancelCtx for parent.
    // It does this by looking up parent.Value(&cancelCtxKey) to find
    // the innermost enclosing *cancelCtx and then checking whether
    // parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
    // has been wrapped in a custom implementation providing a
    // different done channel, in which case we should not bypass it.)
    // parentCancelCtx往树的根节点方向找到最近的context是cancelCtx类型的
    func parentCancelCtx(parent Context) (*cancelCtx, bool) {
        done := parent.Done()
      // closedchan 代表此时cancelCtx 已取消, nil 代表 ctx不是cancelCtx 类型的且不会被取消
        if done == closedchan || done == nil { 
            return nil, false
        }
      // 向上遍历查询canelCtx 类型的ctx
        p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
        if !ok { // 没有
            return nil, false
        }
      // 存在判断信号通道是不是相同
        pdone, _ := p.done.Load().(chan struct{})
        if pdone != done {
            return nil, false
        }
        return p, true
    }

    5.2 canceler

    cancelCtx
    也实现了
    canceler
    接口,实现可以 取消上下文的功能。

    canceler
    接口定义如下:

    // A canceler is a context type that can be canceled directly. The
    // implementations are *cancelCtx and *timerCtx.
    type canceler interface {
        cancel(removeFromParent bool, err error) // 取消
        Done() <-chan struct{} // 只读通道,简称取消信号通道
    }

    cancelCtx
    接口实现如下:

    整体逻辑不复杂,逻辑简化如下:

      当前 cancelCtx 取消 且 与之其关联的子 cancelCtx 也取消

      根据removeFromParent标识来判断是否将子 cancelCtx 移除

    注意

    由于信号通道的初始化采用的懒加载方式,所以有未初始化的情况;

    已初始化的:调用close 函数关闭channel

    未初始化的:用

    closedchan
    初始化,其
    closedchan
    是已经关闭的channel。

    // cancel closes c.done, cancels each of c's children, and, if
    // removeFromParent is true, removes c from its parent's children.
    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
        d, _ := c.done.Load().(chan struct{})
        if d == nil {
            c.done.Store(closedchan)
        } else {
            close(d)
        }
        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()
        if removeFromParent {
            removeChild(c.Context, c)
        }
    }
    // removeChild removes a context from its parent.
    func removeChild(parent Context, child canceler) {
        p, ok := parentCancelCtx(parent)
        if !ok {
            return
        }
        p.mu.Lock()
        if p.children != nil {
            delete(p.children, child)
        }
        p.mu.Unlock()
    }

    closedchan

    可重用的关闭通道,该channel通道默认已关闭

    // closedchan is a reusable closed channel.
    var closedchan = make(chan struct{})
    func init() {
        close(closedchan) // 调用close 方法关闭
    }

    6、timerCtx 源码分析

    cancelCtx
    源码已经分析完毕,那
    timerCtx
    理解起来就很容易。

    关注点

    timerCtx
    是如何取消上下文的,以及取消上下文的方式

    6.1、对象创建 WithDeadline和WithTimeout

    WithTimeout
    底层调用是WithDeadline 方法 ,截止时间是 now+timeout;

    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
        return WithDeadline(parent, time.Now().Add(timeout))
    }

    WithDeadline
    整体逻辑并不复杂,从源码中可分析出
    timerCtx
    取消上下文 采用两种方式 自动手动;其中自动方式采用定时器去处理,到达触发时刻,自动调用cancel方法。

    deadline
    : 截止时间

    timer *time.Timer 
    : 定时器

    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) }
    }
    // A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
    // implement Done and Err. It implements cancel by stopping its timer then
    // delegating to cancelCtx.cancel.
    type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.
        deadline time.Time
    }

    6.2 timerCtx的cancel

      调用cancelCtx的cancel 方法

      根据removeFromParent标识,为true 调用removeChild 方法 从它的父cancelCtx的children中移除

      关闭定时器 ,防止内存泄漏(着重点)

    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()
    }

    7、valueCtx 源码分析

    7.1、对象创建WithValue

    valueCtx
    结构体中有
    key
    val
    两个字段,
    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}
    }
    // A valueCtx carries a key-value pair. It implements Value for that key and
    // delegates all other calls to the embedded Context.
    type valueCtx struct {
        Context
        key, val any
    }

    7.2、获取value值

    func (c *valueCtx) Value(key any) any {
        if c.key == key { // 判断当前valuectx对象中的key是否匹配
            return c.val
        }
        return value(c.Context, key)
    }
    // value() 向根部方向遍历,直到找到与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 == &amp;cancelCtxKey { // 获取cancelCtx对象
                    return c
                }
                c = ctx.Context
            case *timerCtx:
                if key == &amp;cancelCtxKey {
                    return &amp;ctx.cancelCtx
                }
                c = ctx.Context
            case *emptyCtx:
                return nil
            default:
                return c.Value(key)
            }
        }
    }

    标签: golang

    热门推荐