zoukankan      html  css  js  c++  java
  • 图解Go语言的context了解编程语言核心实现源码

    基础筑基

    基于线程的编程语言中的一些设计

    ThreadGroup

    image.png
    ThreadGroup是基于线程并发的编程语言中常用的一个概念,当一个线程派生出一个子线程后通常会加入父线程的线程组(未指定线程组的情况下)中, 最后可以通过ThreadGroup来控制一组线程的退出等操作, 然后在go语言中goroutine没有明确的这种parent/children的关系,如果想退出当前调用链上的所有goroutine则需要用到context

    ThreadLocal

    在基于线程的编程语言语言中,通常可以基于ThreadLocal来进行一些线程本地的存储,本质上是通过一个Map来进行key/value的存储,而在go里面并没有ThreadLocal的设计,在key/value传递的时候,除了通过参数来进行传递,也可以通过context来进行上下文信息的传递

    context典型应用场景

    场景 实现 原理
    上下文信息传递 WithValue 通过一个内部的key/value属性来进行键值对的保存,不可修改,只能通过覆盖的方式来进行值得替换
    退出通知 WithCancel 通过监听通知的channel来进行共同退出的通知

    上下文数据的递归获取

    image.png
    因为在go的context里面并没有使用map进行数据保存,所以实际获取的时候,是从当前层开始逐层的进行向上递归,直至找到某个匹配的key

    其实我们类比ThreadGroup,因为goroutine本身并没有上下级的概念,但其实我们可以通过context来实现传递数据的父子关系,可以在一个goroutine中设定context数据,然后传递给派生出来的goroutine

    取消的通知

    image.png
    既然通过context来构建parent/child的父子关系,在实现的过程中context会向parent来注册自身,当我们取消某个parent的goroutine, 实际上上会递归层层cancel掉自己的child context的done chan从而让整个调用链中所有监听cancel的goroutine退出

    那如果一个child context的done chan为被初始化呢?那怎么通知关闭呢,那直接给你一个closedchan已经关闭的channel那是不是就可以了呢

    带有超时context

    image.png
    如果要实现一个超时控制,通过上面的context的parent/child机制,其实我们只需要启动一个定时器,然后在超时的时候,直接将当前的context给cancel掉,就可以实现监听在当前和下层的额context.Done()的goroutine的退出

    Background与TODO

    image.png
    Backgroud其实从字面意思就很容易理解,其实构建一个context对象作为root对象,其本质上是一个共享的全局变量,通常在一些系统处理中,我们都可以使用该对象作为root对象,并进行新context的构建来进行上下文数据的传递和统一的退出控制

    那TODO呢?通常我们会给自己立很多的todo list,其实这里也一样,我们虽然构建了很多的todo list, 但大多数人其实啥也不会做,在很多的函数调用的过程中都会传递但是通常又不会使用,比如你既不会监听退出,也不会从里面获取数据,TODO跟Background一样,其背后也是返回一个全局变量

    不可变性

    通常我们使用context都是做位一个上下文的数据传递,比如一次http request请求的处理,但是如果当这次请求处理完成,其context就失去了意义,后续不应该继续重复使用一个context, 之前如果超时或者已经取消,则其状态不会发生改变

    源码实现

    context接口

    type Context interface {
        // Deadline返回一个到期的timer定时器,以及当前是否以及到期
        Deadline() (deadline time.Time, ok bool)
    
        // Done在当前上下文完成后返回一个关闭的通道,代表当前context应该被取消,以便goroutine进行清理工作
        // WithCancel:负责在cancel被调用的时候关闭Done
        // WithDeadline: 负责在最后其期限过期时关闭Done
        // WithTimeout:负责超时后关闭done
        Done() <-chan struct{}
    
        // 如果Done通道没有被关闭则返回nil
        // 否则则会返回一个具体的错误
        // Canceled 被取消
        // DeadlineExceeded 过期
        Err() error
    	// 返回对应key的value
        Value(key interface{}) interface{}
    }
    

    emptyCtx

    emptyCtx是一个不会被取消、没有到期时间、没有值、不会返回错误的context实现,其主要作为context.Background()和context.TODO()返回这种root 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"
    }
    

    比较有意思的实现时emptyCtx的String方法,该方法可以返回当前context的具体类型,比如是Background还是TODO, 因为background和todo是两个全局变量,这里通过取其地址来进行对应类型的判断

    cancelCtx

    image.png

    结构体

    cancelCtx结构体内嵌了一个Context对象,即其parent context,同时内部还通过children来保存所有可以被取消的context的接口,后续当当前context被取消的时候,只需要调用所有canceler接口的context就可以实现当前调用链的取消

    type cancelCtx struct {
        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
    }
    
    

    Done

    Done操作返回当前的一个chan 用于通知goroutine退出

    
    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
    }
    

    cancel

    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
        if err == nil {
            panic("context: internal error: missing cancel error")
        }
        // context一旦被某个操作操作触发取消后,就不会在进行任何状态的修改
        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当前chan
            close(c.done)
        }
        // 调用所有children取消
        for child := range c.children {
            child.cancel(false, err)
        }
        c.children = nil
        c.mu.Unlock()
    
        // 是否需要从parent context中移除,如果是当前context的取消操作,则需要进行该操作
        // 否则,则上层context会主动进行child的移除工作
        if removeFromParent {
            removeChild(c.Context, c)
        }
    }
    

    timerCtx

    image.png
    timerCtx主要是用于实现WithDeadline和WithTimer两个context实现,其继承了cancelCtx接口,同时还包含一个timer.Timer定时器和一个deadline终止实现

    2.4.1 结构体

    timerCtx

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

    取消方法

    取消方法就很简单了首先进行cancelCtx的取消流程,然后进行自身的定时器的Stop操作,这样就可以实现取消了

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

    valueCtx

    其内部通过一个key/value进行值得保存,如果当前context不包含着值就会层层向上递归

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

    propagateCancel

    设计目标

    propagateCancel主要设计目标就是当parent context取消的时候,进行child context的取消, 这就会有两种模式:
    1.parent取消的时候通知child进行cancel取消
    2.parent取消的时候调用child的层层递归取消

    parentCancelCtx

    context可以任意嵌套组成一个N层树形结构的context, 结合上面的两种模式,当能找到parent为cancelCtx、timerCtx任意一种的时候,就采用第二种模式,由parent来调用child的cancel完成整个调用链的退出,反之则采用第一种模式监听Done

    func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    	for {
    		switch c := parent.(type) {
    		case *cancelCtx:
    			return c, true	// 找到最近支持cancel的parent,由parent进行取消操作的调用
    		case *timerCtx:
    			return &c.cancelCtx, true // 找到最近支持cancel的parent,由parent进行取消操作的调用
    		case *valueCtx:
    			parent = c.Context // 递归
    		default:
    			return nil, false
    		}
    	}
    }
    

    核心实现

    func propagateCancel(parent Context, child canceler) {
    	if parent.Done() == nil {
    		return // parent is never canceled
    	}
    	if p, ok := parentCancelCtx(parent); ok {
    		p.mu.Lock()
    		if p.err != nil {
    			// parent has already been canceled
                // 如果发现parent已经取消就直接进行取消
    			child.cancel(false, p.err)
    		} else {
    			if p.children == nil {
    				p.children = make(map[canceler]struct{})
    			}
                // 否则加入parent的children map中
    			p.children[child] = struct{}{}
    		}
    		p.mu.Unlock()
    	} else {
    		go func() {
    			select {
    			case <-parent.Done():
                    // 监听parent DOne完成, 此处也不会向parent进行注册
    				child.cancel(false, parent.Err())
    			case <-child.Done():
    			}
    		}()
    	}
    }
    

    WithDeadline

    有了上面的基础学习WithDeadline,就简单了许多, WithDeadline会给定一个截止时间, 可以通过当前时间计算需要等待多长时间取消即可

    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    	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,
    	}
        // 监听parent的取消,或者向parent注册自身
    	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() {
                // 构建一个timer定时器,到期后自动调用cancel取消
    			c.cancel(true, DeadlineExceeded)
    		})
    	}
        // 返回取消函数
    	return c, func() { c.cancel(true, Canceled) }
    }
    

    Backgroup与TODO

    在很多底层的中间件的调用中都会通过context进行信息的传递,其中最常用的就是Backgroup和Todo, 虽然都是基于emptyCtx实现,但Backgroup则更倾向于作为一个parent context进行后续整个调用链context的root使用,而TODO通常则表明后续不会进行任何操作,仅仅是因为参数需要传递使用

    原文链接 http://www.sreguide.com/go/context.html
    微信号:baxiaoshi2020
    关注公告号阅读更多源码分析文章 21天大棚
    更多文章关注 www.sreguide.com
    本文由博客一文多发平台 OpenWrite 发布

  • 相关阅读:
    目标跟踪学习笔记1
    求职笔记
    Pycharm使用问题小结-002
    Pycharm使用问题小结-001
    Python基础练习-004-百变乘法表
    Python基础练习-003-求100-999之间所有的水仙花数
    Python基础练习-002-求1000以内的完全数
    Python基础练习-001-猜数字小游戏
    java.io.FileNotFoundException: rmi_keystore.jks (No such file or directory)(转)
    Jmeter
  • 原文地址:https://www.cnblogs.com/buyicoding/p/12155169.html
Copyright © 2011-2022 走看看