图解Go语言的context了解编程语言核心实现源码(go语言程序设计 豆瓣)

网友投稿 245 2022-06-17


基础筑基

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

ThreadGroup

ThreadLocal

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

context典型应用场景

场景

实现

原理

上下文信息传递

WithValue

通过一个内部的key/value属性来进行键值对的保存,不可修改,只能通过覆盖的方式来进行值得替换

退出通知

WithCancel

通过监听通知的channel来进行共同退出的通知

上下文数据的递归获取

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

取消的通知

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

带有超时context

Background与TODO

那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

结构体

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

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通常则表明后续不会进行任何操作,仅仅是因为参数需要传递使用


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Golang学习之GOROOT、PATH、GOPATH及go get(goat paths)
下一篇:Dropbox的GO语言之旅:可靠性和持久性
相关文章

 发表评论

暂时没有评论,来抢沙发吧~