zoukankan      html  css  js  c++  java
  • golang学习笔记---上下文 context

    golang 的 Context 包,是专门用来简化多个goroutine之间的上下文同步。

    库的介绍
    Go 语言中的每一个请求的都是通过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的 Goroutine 访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、同时优雅地结束上下文。

    一个实际例子是,在Go服务器程序中,每个请求都会有一个goroutine去处理。然而,处理程序往往还需要创建额外的goroutine去访问后端资源,比如数据库、RPC服务等。由于这些goroutine都是在处理同一个请求,所以它们往往需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等。而且如果请求超时或者被取消后,所有的goroutine都应该马上退出并且释放相关的资源。这种情况也需要用Context来为我们取消掉所有goroutine。

    一句话:Context 包可以在不同的 Goroutine 之间同步请求数据,还能优雅地设置超时及信号来结束上下文。 

    标准库的context

    从设计角度上来讲, golang的context包提供了一种父routine对子routine的管理功能. 我的这种理解虽然和网上各种文章中讲的不太一样, 但我认为基本上还是很贴合实际的.

    context包中定义了一个很重要的接口, 叫context.Context.它的使用逻辑是这样的:

    1. 当父routine需要创建一个子routine的时候, 父routine应当先创建一个context.Context的实例, 这个实例中包括的内容有:
      1. 对子routine生命周期的限制: 比如子routine应该什么时候自杀, 什么条件下自杀. 在服务端编程中, 一个生动的例子就是: 接收请求的routine在将请求派发给工作routine的时候, 需要告诉工作routine: 超过400ms没处理完你就给我就地爆炸.
      2. 将一些数据共享给子routine.
      3. 在子routine运行过程中, 通过这个Context实例, 可以干涉子routine的生命周期
    2. 子routine拿到父routine创建的context.Context实例后, 开始干活, 干活的过程中, 需要:
      1. 遵守Context实例中关于自身生命周期的约束: 400ms请求没有处理完, 我要就地爆炸
      2. 在自杀之前将自己自杀的消息传递给Context, 这样父routine就可以得知自己的生命状态. 比如我200ms处理完了请求, 我要告诉父routine, 我已经好了
      3. 工作的时候, 如有必要, 从Context中获取一些必要数据.
      4. 工作结束时, 如有必要, 将一些工作成果发送给Context, 以让父routine得知: 比如, 我处理这个请求花费的时间是197ms
      5. 在运行过程中, 从Context接收来自你routine的调度信号

    所以说很显然:

    1. Context实例是由父routine创建的. 创建之后传递给子routine作为行为规范
    2. 子routine一般是不允许操作这个Context实例的. 子routine应当耐心倾听, 仅在必要的时候, 比如自杀之前, 将一些信息传递给Context
    3. 一个Context的一生, 从生到死, 是和子routine绑定在一起的. 子routine生, Context生, 子routine死, Context
    4. 良好设计的服务端程序, 每个routine都应该有自己的Context. 而既然routine之间有父子关系树, 那么显然所有routine的Context之间也有一坨树型关系.

    我们现在来看context/context.go中是如何实现这套工具的

    1 首先是对基本Context的定义

    // 定义了一个接口, 名为Context
    type Context interface {
        // 返回这个Context的死亡时刻, 如果ok == false, 则这个Context是永生的
        Deadline() (deadline time.Time, ok bool)
        
        // 返回一个channel, 这个channel在Context被Cancel的时候被关闭
        // 如果Context是永生的, 则返回一个nil
        Done() <-chan struct{}
        
        // 在Context活着的时候, (Done()返回的channel还没被关闭), 它返回nil
        // 在Context死后, (Done()返回的channel被关闭), 它返回一个error实例用以说明:
        //   这个Context是为什么死掉的, 是被Cancel, 还是自然死亡?
        Err() error
        
        // 返回存储在Context中的通信数据
        // 注意: 不要滥用这个接口, 它不是用来给子routine传递参数用的!
        Value(key interface{}) interface
    }
     
    // 定义了两个error实例, 并为其中一个实例的error类型定义了三个方法
    var Canceled = errors.New("context canceled") // 用以在Context被Cancel时, 从Err()返回
    var DeadlineExceeded error = deadlineExceedError{} // 用以在Context自然死亡时, 从Err()返回
    type deadlineExceedError struct{}
    func (deadlineExceededError) Error() string   { return "context deadline exceeded" } // 实现error接口
    func (deadlineExceededError) Timeout() bool   { return true }
    func (deadlineExceededError) Temporary() bool { return true }
     
    // 实现了一个Context类型: emptyCtx, 它有以下特点:
    //  0. 这个类型不对外公开, 仅通过后面的两个接口公开它的两个实例
    //  1. 不能被Cancel
    //  2. 也从不自然死亡, 它是永生的
    //  3. 不同的实例之间需要有不同的地址, 所以它没有被定义成struct{}, 而是用一个int来替代
    //  4. 它内部也不存储任何数据
    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
    }
     
    // 定义了两个emptyCtx的实例, 并写了两个接口对外公开这两个实例
    var (
        background = new(emptyCtx)
        todo       = new(emptyCtx)
    )
     
    func (e *emptyCtx) String() string {
        switch e {
        case background:
            return "context.Background"
        case todo:
            return "context.TODO"
        }
        return "unknown empty Context"
    }
     
    func Background() Context {
        return background
    }
     
    func TODO() Context {
        return todo
    }
    

      

    上面定义了Context的接口规范, 也定义了一个Context接口的实现: emptyCtx, 从代码上可以看出来, 标准库并不公开这个emptyCtx的实现, 你只能从它的公开接口context.Background()context.TODO()来访问两个已经实例化的emptyCtx实例.

    这两个实例是用于为顶层routine使用的.下面我们再来看, 可被创建者Cancel的Context是怎么实现的

    Context接口的实现: 支持Cancel操作的Context: 非公开类cancelCtx

    首先是类定义

    type cancelCtx struct {
        Context                        // 他爹
     
        mu       sync.Mutex            // 一个互斥锁, 用来保护其它字段
        done     chan struct{}         // Done()方法的返回值
        children map[canceler]struct{} // 这里记录了它的孩子
        err      error                 // Err()方法的返回值
    }
    

    我们在上面说了, 由于程序中的routine之间是有父子关系树存在的, 那么一个context正常情况下就有可能有孩子, 那么, 如果当前的routine持有的Context实例是可被Cancel的, 那么显然, 它的所有孩子routine, 也应当是可被Cancel的.

    这就是为什么cancelCtx类中有Context字段和children字段的原因, 也是为什么children字段是一个map[canceler]struct{}类型的原因: key中记录着所有的孩子, value是没有意义的, 为什么这样写呢? 因为这里把map当成C++中的std::set在用!

    key的类型canceler是一个接口, 一个表示Context必须可被Cancel的接口:

    type canceler interface {
        cancel(removeFromParent bool, err error)
        
        // Context接口中的Done方法
        Done() <-chan struct{}
    }
    

      

    显然, cancelCtx类本身也是可被Cancel的, 所以它也要实现canceler这个接口

    下面是cancelCtx类的方法实现:

    // Context.Done的实现: 返回字段 done
    func (c *cancelCtx) Done() <-chan struct{} {
        c.mu.Lock() // 锁保护done字段的初始化
        if c.done == nil {
            c.done = make(chan struct{})
        }
        d := c.done
        c.mu.Unlock()
        return d
    }
    // Context.Err的实现
    func (c *cancelCtx) Err() error {
        c.mu.Lock()
        defer c.mu.Unlock()
        return c.err
    }
     
    // String()方法实现
    func (c *cancelCtx) String() string {
        return fmt.Sprintf("%v.WithCancel", c.Context)
    }
     
    // canceler.cancel接口实现
    // 参数 removeFromParent 指示是否需要把它从它爹的孩子中除名
    // 参数 err 将赋值给字段 err, 以供Context.Err方法返回
    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 {   // 如果err字段有值, 则说明已经被Cancel了
            c.mu.Unlock()
            return // already canceled
        }
        c.err = err
        if c.done == nil {  // 设置c.done, 以供Done方法返回
            c.done = closedchan
        } else {
            close(c.done)
        }
        
        // 挨个cancel它的所有孩子, 子随父死的时候, 并不除名父子关系
        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)
        }
    }
     
    // 这是一个全局复用的, 被关闭的channel, 用于被Context.Done返回使用
    var closedchan = make(chan struct{})
     
    func init() {
        close(closedchan)
    }
    

      

    可以看到, cancelCtx本身并没有实现所有的Context接口中的方法. 其余没有实现的接口是通过Context这个没有指定字段名的字段实现的. 这是go的特殊语法糖: 继承接口.

    在一个类型定义中, 声明一个接口类型字段, 并且还不指定字段的名称, 这代表

    1. 当前类型必然实现了接口类型
    2. 当调用接口方法时, 默认调用的是子字段的方法, 除非当前类型显式overwrite了一些方法的实现

    其实就是一种更为灵活的继承写法

    我们再来看, 当父routine需要创建一个带有Cancel功能的Context实例的时候, 应该怎么办:

    // 首先是定义一个函数指针别名
    type CancelFunc func()
    // 再就是父routine创建带Cancel功能的子Context的函数
    // 父routine将自己的Context实例传入, 这个函数会返回子Context(带Cancel功能)
    // 还会返回一个可调用对象 cancel, 调用这个对象(函数), 就能达到Cancel的功能
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)   // 创建一个cancelCtx的实例
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
    }
     
    // 下面是WithCancel中引用的两个私有函数的实现
     
    // 创建一个cancelCtx实例
    func newCancelCtx(parent Context) cancelCtx {
        return cancelCtx{Context: parent}   // 把爹先记录下来
    }
     
    func propagateCancel(parent Context, child canceler) {
        // 如果父Context是不可Cancel, 什么也不做
        if parent.Done() == nil {
            return // parent is never canceled
        }
        // 如果父Context本身是可Cancel的
        if p, ok := parentCancelCtx(parent); ok {
            // 进入此分支, 说明父Context是以下三种之一:
            //  1. 是一个cancelCtx, 本身就可被Cancel
            //  2. 是一个timerCtx, timerCtx是canctx的一个子类, 也可被Cancel
            //  3. 是一个valueCtx, valueCtx继承体系上的某个爹, 是以上两者之一
            // 那么p就是那个父Context的继承体系中的cancelCtx实例
            p.mu.Lock()
            if p.err != nil {
                // 若p已经被Cancel或自然死亡, 作为儿子, 就必须死了
                // 直接调用p.cancel
                child.cancel(false, p.err)
            } else {
                // 若p还活着, 就把儿子添加到它的儿子列表中去
                if p.children == nil {
                    p.children = make(map[canceler]struct{})
                }
                p.children[child] = struct{}{}
            }
            p.mu.Unlock()
        } else {
            // 进入此分支, 说明父Context虽然可被Cancel
            // 但并不是标准库中预设的cancelCtx或timerCtx两种可被Cancel的类型
            // 这意味着这个特殊的父Context, 内部并不能保证记录了所有儿子的列表
            // 这里就得新开一个routine, 时刻监视着父Context的生存状态
            // 一旦父Context死亡, 就立即调用child.cancel把儿子弄死
            go func() {
                select {
                case <-parent.Done():   // 如果爹死了, 把孩子弄死
                    child.cancel(false, parent.Err())
                case <-child.Done():    // 如果孩子死了, 什么也不做
                }
            }()
        }
    }
     
    // 判断Context实例是否是一个可被Cancel的类型
    // 标准库中可被Cancel的Context类型共有三种:
    //    1. cancelCtx
    //    2. timerCtx
    // 仅有这两种
    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
            }
        }
    }
    

      

    当你使用WithCancel

    一个简单的例子

    这里来捋一捋, 当你调用WithCancel创建一个可被Cancel的Context实例时, 都发生了些什么:

    // 第一步, 创建者routine本身必须持有一个Context
    // 这里假定创建者就是main routine
    // 我们调用 Background创建一个不可被Cancel, 不会自杀的Context
    contextOfMain := ctx.Background()
     
    // 第二步: 调用WithCancel创建子Context
    contextOfSubRoutine, cancelFuncOfSubRoutine := ctx.WithCancel(contextOfMain)
    

      

      

  • 相关阅读:
    poj 2778 AC自己主动机 + 矩阵高速幂
    Web Services 指南之:Web Services 综述
    SQL多表连接查询(具体实例)
    HibernateUtil
    哈夫曼编码问题再续(下篇)——优先队列求解
    MySQL Merge存储引擎
    程序的入口及AppDelegate窗体显示原理
    几个免费的DNS地址
    kettle与各数据库建立链接的链接字符串
    【转】利用optimize、存储过程和系统表对mysql数据库表进行批量碎片清理释放表空间
  • 原文地址:https://www.cnblogs.com/saryli/p/13358552.html
Copyright © 2011-2022 走看看