图解Go语言的context了解编程语言核心实现源码

基础筑基

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

ThreadGroup

image.png ThreadGroup是基于线程并发的编程语言中经常使用的一个概念,当一个线程派生出一个子线程后一般会加入父线程的线程组(未指定线程组的状况下)中, 最后能够经过ThreadGroup来控制一组线程的退出等操做, 而后在go语言中goroutine没有明确的这种parent/children的关系,若是想退出当前调用链上的全部goroutine则须要用到contexthtml

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那是否是就能够了呢ide

带有超时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, 以前若是超时或者已经取消,则其状态不会发生改变ui

源码实现

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 发布

相关文章
相关标签/搜索