美文网首页
context学习笔记

context学习笔记

作者: 鸿雁长飞光不度 | 来源:发表于2021-05-21 15:10 被阅读0次

    context是Go语言提供的用于控制协程的重要工具包,提供了许多开发中非常实用的功能,下面就对各种各样的context进行分析,并结合案例说明应用场景。

    1. Context概述

    Context其实只是一个接口,凡是实现这个接口的都可以称之为context。该接口包含四个方法,官方对每个方法的功能和职责都做了非常详细的介绍。

    1.Deadline() (deadline time.Time, ok bool)

     //该函数返回代表当前context完成工作后应该被cancel的截止时间,
     //如果没有设置截止时间,ok会等于false,函数多次调用都会返回相同的结果
    Deadline() (deadline time.Time, ok bool)
    

    2.Done() <-chan struct{}

    // 返回一个channel,当代表当前context完成工作后应该被cancel时,channel会被关闭。如果一个上下文
    // 不能被cancel,Done就会返回nil,连续调用会返回相同结果。done函数返回的channel的关闭
    // 可能会在cancel函数返回后异步执行。Done函数通常依赖select函数实现需求。
    //这里可以看到更多关于Done函数cancel的案例: https://blog.golang.org/pipelines
        Done() <-chan struct{}
    

    使用案例

    
    //Stream函数调用DoSomething函数生成values,
    //然后把这些values发给名称为out的channel,
    //直到context返回一个错误或者ctx.Done的channel被关闭。
    func Stream(ctx context.Context, out chan<- Value) error {
        for {
            v, err := DoSomething(ctx)
            if err != nil {
                return err
            }
            select {
            case <-ctx.Done():
                return ctx.Err()
            case out <- v:
            }
        }
    }
    
    1. Err() error
    //如果Done返回的channel没有被关闭,Err函数返回nil,
    //如果关闭了,Err会返回一个非nil的error解释原因:
    //如果context是cancel了,error就是Canceled
    //如果是截止时间到了,error就是DeadlineExceeded
    //在Err返回一个非nil的error后,连续调用会得到相同的结果。
    Err() error
    

    4.Value(key interface{}) interface{}

    //Value函数返回一个和当前context关联的key的value,如果没有value和key关联返回nil
    //连续调用Value方法如果key相同,返回结果也相同。
    //context的value仅用于请求数据的传输流程和api边界,不是用来给函数传递可选字段的。
    //一个key代表了在一个context的指定的value,对于希望存储values在context里面的函数,
    //通常是声明一个全局变量做key,然后把这个key作为参数,调用context.WithValue()、
    //context.Value(),分别实现存值和取值。key的类型可以是任意支持相等比较任意类型
    //使用者的包定义的这些key应该是非导出类型从而避免key冲突。
    //使用者的包定义一个context的key应该提供一个类型安全的获取函数来说访问存储的value
    Value(key interface{}) interface{}
    

    2.Context类型

    通过上面接口职责说明里面可以看到,不是所有的context都需要完全实现上面的方法,为了实现接口而去实现不需要的方法就会很多余,go语言通过一个empty类型的context解决了这个问题,下面会具体分析。

    1. emptyCtx
    • 使用:调用context.Background()方法来生成。(内部直接返回包内的全局变量emptyCtx类型的background)

    • 作用:代表一个空的context。通常用来作为生成其他类型的context的父context。

    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
    }
    
    func (e *emptyCtx) String() string {
        switch e {
        case background:
            return "context.Background"
        case todo:
            return "context.TODO"
        }
        return "unknown empty Context"
    }
    

    2.cancelCtx

    • 使用: 调用 context.WithCancel(context.Background())格式 生成。
    • 作用:是一个可以执行cancel的上下文,通过消息对协程进行统一控制。
      //初始化一个cancelCtx实例
      //如果父context也支持cancel,添加到父节点上。
     // 返回cancelCtx实例和cancel方法。
     func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
     }
    

    添加到父parent函数的实现

    func propagateCancel(parent Context, child canceler) {
        done := parent.Done() //
        if done == nil {
            return //说明parent不支持cancel,直接返回吧
        }
    
        select {
        case <-done:
            // parent已经cancel了,当前也执行cancel,并传递父parent的err
            child.cancel(false, parent.Err())
            return
        default:
        }
            // 查找支持parent的父节点
        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{})
                }
                            //如果父parent支持cancel
                p.children[child] = struct{}{}
            }
            p.mu.Unlock()
        } else { // 如果parent的的父节点都不支持cancel
            atomic.AddInt32(&goroutines, +1)// 当前包内的协程数量开启数+1
            go func() {
                select {
                case <-parent.Done(): // 等待parent节点结束
                    child.cancel(false, parent.Err()) // 在把当前的结束。
                case <-child.Done():
                }
            }()
        }
    }
    

    parentCancelCtx函数实现。

    // 返回父级的中最基础的*cancelCtx,通过调用parent.Value(&cancelCtxKey)
    // 寻找最里面的*cancelCtx,然后检查parent.Done()是否和*cancelCtx匹配,
    // 如果不匹配,说明*cancelCtx被一个自定义实现包装过了,提供了一个不同的done 
    // channel,在这种情况下我们不应该绕过它。
    func parentCancelCtx(parent Context) (*cancelCtx, bool) {
        done := parent.Done()
        if done == closedchan || done == nil {
            return nil, false
        }
        p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
        if !ok {
            return nil, false
        }
        p.mu.Lock()
        ok = p.done == done
        p.mu.Unlock()
        if !ok {
            return nil, false
        }
        return p, true
    }
    
    //  这是一个可以被取消的context,当调用Cancel的时候,所有的实现cancler接口的子context也会调用cancel
    type cancelCtx struct {
    // 这里是一个接口,蕴含的形式定义的,通常我们可以用emptyCtx来往这里放,
    // emptyCtx完整实现了context,所以凡是这样写的都可以按重写需要的接口方法,比如
    // 这个就没有重写deadLine方法,但是依然时context,其他类型的context都采用了类似形式。
        Context  
        mu       sync.Mutex            // protects following fields
        done     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
    }
    
    // 他本身
    func (c *cancelCtx) Value(key interface{}) interface{} {
        if key == &cancelCtxKey { // 这个key专门返回自己
            return c
        }
        return c.Context.Value(key) // 其他的调用c.Context.Value()方法实现。
    }
    
    // c.done可能是nil,生命周期nil,chan,close
    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
    }
    
    // 返回错误原因
    func (c *cancelCtx) Err() error {
        c.mu.Lock()
        err := c.err
        c.mu.Unlock()
        return err
    }
    

    cancelCtx还实现了canceler,这接口代表了可以cancel的context应该实现的方法。

    type canceler interface {
            // 取消函数(是否从父节点移出,err是取消原因,不能为nil)
        cancel(removeFromParent bool, err error) // 
           // 完成以后发的消息。
        Done() <-chan struct{}
    }
    

    对cancel方法的实现

    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
        if c.done == nil {
            c.done = closedchan //全局变量
        } else {
            close(c.done)
        }
        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)
        }
    }
    

    使用案例

        var ctx, cancel = context.WithCancel(context.Background())
        for i := 0; i < 2; i++ {
            go func(ctx context.Context, i int) {
    
                go func(ctx2 context.Context,i2 int) {
                    for ; ;  {
                        select {
                        case <-ctx.Done():
                            err := ctx.Err()
                            fmt.Printf("i2_1 function done value %v err %s\n", i2,err.Error())
                            return
                        default:
                            fmt.Println("i2_1 doing")
                            //time.Sleep(5*time.Microsecond)
                        }
    
                    }
                }(ctx,i)
    
                go func(ctx2 context.Context,i2 int) {
                    for ; ;  {
                        select {
                        case <-ctx.Done():
                            err := ctx.Err()
                            fmt.Printf("i2_2 function done value %v err %s\n", i2,err.Error())
                            return
                        default:
                            fmt.Println("i2_2 doing")
                            //time.Sleep(5*time.Microsecond)
                        }
                    }
    
                }(ctx,i)
    
            }(ctx,i)
        }
        time.Sleep(100*time.Microsecond) // 模拟逻辑执行
        cancel() // 取消以后,创建的协程会收到取消通知
        time.Sleep(1000*time.Microsecond) // 模拟后续执行流程
    

    创建了多个协程,包括嵌套创建的。然后在协程里面执行逻辑,并监听ctx.Done的消息。通过调用cancel方法每个协程都收到ctx的cancel消息了。
    输出结果如下

    i2_2 doing
    i2_2 doing
    i2_1 doing
    i2_2 doing
    i2_2 function done value 1 err context canceled
    i2_1 function done value 0 err context canceled
    i2_2 function done value 0 err context canceled
    i2_1 function done value 1 err context canceled
    
    1. timerCtx
    • 使用:调用WithDeadline或者WithTimeout方法
    • 作用:timerCtx基于cancelCtx实现,增加了deadline用于标记自动cancel截止时间,timer就是对时间的计时器。
    type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.
    
        deadline time.Time
    }
    
    //WithDeadline返回带有一个调整了不迟于d的截止日期的父context的拷贝,如果父context截止日期
    // 已经早于d,等同于返回的是parent,返回的上下文里面的Done channel会在截止时间过期、
    // 或者返回的cancel函数被手动执行,或者parent的context的Done channel被关闭时关闭。
    // 不论哪一个情况第一次发生都会关闭。
    
    // canceling一个context会释放和它关联的资源,所以代码应该在完成在context的操作后立即调用。
    
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
        if cur, ok := parent.Deadline(); ok && cur.Before(d) {
            // 当前设置的时间比parent的早,用parent的
            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) }
    }
    
    // WithTimeout 返回的结果是 WithDeadline(parent, time.Now().Add(timeout)).
    //
    // 取消context操作将会释放和它关联的资源, 所以代码里面应该尽可能早的在上线文中调用cancel:
    //
    //  func slowOperationWithTimeout(ctx context.Context) (Result, error) {
    //      ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    //      defer cancel()  //如果slowOperation在超时之前完成释放资源。
    //      return slowOperation(ctx)
    //  }
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
        return WithDeadline(parent, time.Now().Add(timeout))
    }
    

    接口实现

    // 直接返回截止时间
    func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
        return c.deadline, true
    }
    
    // 和cancelCtx差不多,只是关闭了定时器。
    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()
    }
    

    4.valueCtx

    • 使用:context.WithValue()
    • 作用:各个协程之间传递一些数据, 只需要实现context的value接口。
    // WithValue returns a copy of parent in which the value associated with key is
    // val.
    // 设置值
    func WithValue(parent Context, key, val interface{}) Context {
        if key == nil {
            panic("nil key")
        }
            // key是可以用来相等比较
        if !reflectlite.TypeOf(key).Comparable() {
            panic("key is not comparable")
        }
        return &valueCtx{parent, key, val}
    }
    
    func (c *valueCtx) Value(key interface{}) interface{} {
        if c.key == key {
            return c.val
        }
            // 会从父context去查询。
        return c.Context.Value(key)
    }
    
    

    使用案例,下面的案例来着官方

    // user包定义了一个User类型存储在context。
    package user
    
    import "context"
    
    // User 是存储在context里面的值。
    type User struct {...}
    
    // key定义了非导出的数据类型,仅包内可见,
    // 这避免了和其他包的冲突
    type key int
    
    // userKey是代表在context里面的user.User作为value时对应的key,也是非导出的,
    // 使用者应该使用 ser.NewContext和user.FromContext函数,而不是直接使用这个key
    var userKey key
    // 返回一个新的context并且设置一个value u.
    func NewContext(ctx context.Context, u *User) context.Context {
        return context.WithValue(ctx, userKey, u)
    }
    
    // 获取存储在context里面的User类型的value。
    func FromContext(ctx context.Context) (*User, bool) {
        u, ok := ctx.Value(userKey).(*User)
        return u, ok
    }      
    

    总结:

    • Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;
    • cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
    • timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
    • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例; - - 三种context实例可互为父节点,从而可以组合成不同的应用形式;

    相关文章

      网友评论

          本文标题:context学习笔记

          本文链接:https://www.haomeiwen.com/subject/eoxbjltx.html