热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

python教程分享对Go语言中的context包源码分析

目录一、包说明分析二、包结构分析三、context接口类型分析四、后续分析规划五、基于实现类型到常用函数六、with-系列函数七、扩展功能以及如何扩展八、补充一、包说明分析cont
目录
  • 一、包说明分析
  • 二、包结构分析
  • 三、context接口类型分析
  • 四、后续分析规划
  • 五、基于实现类型到常用函数
  • 六、with-系列函数
  • 七、扩展功能以及如何扩展
  • 八、补充

一、包说明分析

context包:这个包分析的是1.15

context包定义了一个context类型(接口类型),通过这个context接口类型, 就可以跨api边界/跨进程传递一些deadline/cancel信号/request-scoped值.

发给server的请求中需要包含context,server需要接收context. 在整个函数调用链中,context都需要进行传播. 期间是可以选择将context替换为派生context(由with-系列函数生成). 当一个context是canceled状态时,所有派生的context都是canceled状态.

with-系列函数(不包含withvalue)会基于父context来生成一个派生context, 还有一个cancelfunc函数,调用这个cancelfun函数可取消派生对象和 "派生对象的派生对象的…",并且会删除父context和派生context的引用关系, 最后还会停止相关定时器.如果不调用cancelfunc,直到父context被取消或 定时器触发,派生context和"派生context的派生context…"才会被回收, 否则就是泄露leak. go vet工具可以检测到泄露.

使用context包的程序需要遵循以下以下规则,目的是保持跨包兼容, 已经使用静态分析工具来检查context的传播:

  • context不要存储在struct内,直接在每个函数中显示使用,作为第一个参数,名叫ctx
  • 即使函数允许,也不要传递nil context,如果实在不去确定就传context.todo
  • 在跨进程和跨api时,要传request-scoped数据时用context value,不要传函数的可选参数
  • 不同协程可以传递同一context到函数,多协程并发使用context是安全的

二、包结构分析

核心的是:

    func withcancel(parent context) (ctx context, cancel cancelfunc)      func withdeadline(parent context, d time.time) (context, cancelfunc)      func withtimeout(parent context, timeout time.duration) (context, cancelfunc)      type cancelfunc      type context

从上可以看出,核心的是context接口类型,围绕这个类型出现了with-系列函数, 针对派生context,还有取消函数cancelfunc.

还有两个暴露的变量:

canceled

context取消时由context.err方法返回

deadlineexceeded

context超过deadline时由context.err方法返回

三、context接口类型分析

context也称上下文.

 type context interface {        deadline() (deadline time.time, ok bool)        done() <-chan struct{}        err() error        value(key interface{}) interface{}      }

先看说明:

跨api时,context可以携带一个deadline/一个取消信号/某些值. 并发安全.

方法集分析:

deadline

  • 返回的是截至时间
  • 这个时间表示的是任务完成时间
  • 到这个时间点,context的状态已经是be canceled(完成状态)
  • ok为false表示没有设置deadline
  • 连续调用,返回的结果是相同的

done

  • 返回的只读信道
  • 任务完成,信道会被关闭,context状态是be canceled
  • conetext永远不be canceled,done可能返回nil
  • 连续调用,返回的结果是相同的
  • 信道的关闭会异步发生,且会在取消函数cancelfunc执行完之后发生
  • 使用方面,done需要配合select使用
  • 更多使用done的例子在这个博客

err

  • done还没关闭(此处指done返回的只读信道),err返回nil
  • done关闭了,err返回non-nil的error
  • context是be canceled,err返回canceled(这是之前分析的一个变量)
  • 如果是超过了截至日期deadline,err返回deadlineexceeded
  • 如果err返回non-nil的error,后续再次调用,返回的结果是相同的

value

  • 参数和返回值都是interface{}类型(这种解耦方式值得学习)
  • value就是通过key找value,如果没找到,返回nil
  • 连续调用,返回的结果是相同的
  • 上下文值,只适用于跨进程/跨api的request-scoped数据
  • 不适用于代替函数可选项
  • 一个上下文中,一个key对应一个value
  • 典型用法:申请一个全局变量来放key,在context.withvalue/context.value中使用
  • key应该定义为非暴露类型,避免冲突
  • 定义key时,应该支持类型安全的访问value(通过key)
  • key不应该暴露
    • 表示应该通过暴露函数来进行隔离(具体可以查看源码中的例子)

四、后续分析规划

看完context的接口定义后,还需要查看with-系列函数才能知道context的定位, 在with-系列中会涉及到context的使用和内部实现,那就先看withcancel.

withcancel:

  • cancelfunc
  • newcancelctx
    • cancelctx
  • canceler
  • propagatecancel
    • parentcancelctx

以下是分析出的通过规则:很多包对外暴露的是接口类型和几个针对此类型的常用函数. 接口类型暴露意味可扩展,但是想扩展之后继续使用常用函数,那扩展部分就不能 修改常用函数涉及的部分,当然也可以通过额外的接口继续解耦. 针对"暴露接口和常用函数"这种套路,实现时会存在一个非暴露的实现类型, 常用函数就是基于这个实现类型实现的.在context.go中的实现类型是emptyctx. 如果同时需要扩展接口和常用函数,最好是重新写一个新包.

下面的分析分成两部分:基于实现类型到常用函数;扩展功能以及如何扩展.

五、基于实现类型到常用函数

context接口的实现类型是emptyctx. 

  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除了实现了context.context接口,还实现了context.stringer接口, 注意下面的string不是实现的fmt.stringer接口,而是未暴露的context.stringer接口. 正如empty的名字,对context接口的实现都是空的,后续需要针对emptyctx做扩展.

    var (        background = new(emptyctx)        todo       = new(emptyctx)      )      func background() context {        return background      }      func todo() context {        return todo      }

这里通过两个暴露的函数创建两个空的emptyctx实例,后续会根据不同场景来扩展. 在注释中,background实例的使用场景是:main函数/初始化/测试/或者作为top-level 的context(派生其他context);todo实例的使用场景是:不确定时用todo. 到此emptyctx的构造就理顺了,就是background()/todo()两个函数,之后是针对她们 的扩展和context派生.

context派生是基于with-系列函数实现的,我们先看对emptyctx的扩展, 这些扩展至少会覆盖一部分函数,让空的上下文变成支持某些功能的上下文, 取消信号/截至日期/值,3种功能的任意组合.

从源码中可以看出,除了emptyctx,还有cancelctx/myctx/mydonectx/othercontext/ timectx/valuectx,他们有个共同特点:基于context组合的新类型, 我们寻找的对emptyctx的扩展,就是在这些新类型的方法中.

小技巧:emptyctx已经实现了context.context,如果要修改方法的实现, 唯一的方法就是利用go的内嵌进行方法的覆盖.简单点说就是内嵌到struct, struct再定义同样签名的方法,如果不需要数据,内嵌到接口也是一样的.

cancelctx

支持取消信号的上下文

type cancelctx struct {    context      mu       sync.mutex    done     chan struct{}    children map[canceler]struct{}    err      error  }

看下方法:

  var cancelctxkey int      func (c *cancelctx) value(key interface{}) interface{} {        if key == &cancelctxkey {          return c        }        return c.context.value(key)      }      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      }

cancelctxkey默认是0,value()要么返回自己,要么调用上下文context.value(), 具体使用后面再分析;done()返回cancelctx.done;err()返回cancelctx.err;

    func contextname(c context) string {        if s, ok := c.(stringer); ok {          return s.string()        }        return reflectlite.typeof(c).string()      }      func (c *cancelctx) string() string {        return contextname(c.context) + ".withcancel"      }

internal/reflectlite.typeof是获取接口动态类型的反射类型, 如果接口是nil就返回nil,此处是获取context的类型, 从上面的分析可知,顶层context要么是background,要么是todo, cancelctx实现的context.stringer要么是context.background.withcancel, 要么是context.todo.withcancel.这里说的只是顶层context下的, 多层派生context的结构也是类似的.

值得注意的是string()不属于context接口的方法集,而是emptyctx对 context.stringer接口的实现,cancelcxt内嵌的context,所以不会覆盖 emptyctx对string()的实现. 

   var closedchan = make(chan struct{})      func init() {        close(closedchan)      }        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)        }      }

cancel(),具体的取消信令对应的操作,err不能为nil,err会存到cancelctx.err, 如果已经存了,表示取消操作已经执行.关闭done信道,如果之前没有调用done() 来获取done信道,就返回一个closedchan(这是要给已关闭信道,可重用的), 之后是调用children的cancel(),最后就是在context树上移除当前派生context.

    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      }      func removechild(parent context, child canceler) {        p, ok := parentcancelctx(parent)        if !ok {          return        }        p.mu.lock()        if p.children != nil {          delete(p.children, child)        }        p.mu.unlock()      }

removechild首先判断父context是不是cancelctx类型, 再判断done信道和当前context的done信道是不是一致的, (如果不一致,说明:done信道是diy实现的,就不能删掉了).

到此,cancelctx覆盖了cancelctx.context的done/err/value, 同时实现了自己的打印函数string(),还实现了cancel(). 也就是说cancelctx还实现了接口canceler:

type canceler interface {    cancel(removefromparent bool, err error)    done() <-chan struct{}  }  // cancelctx.children的定义如下:  // children map[canceler]struct{}

执行取消信号对应的操作时,其中有一步就是执行children的cancel(), children的key是canceler接口类型,所以有对cancel()的实现. cancelctx实现了canceler接口,那么在派生context就可以嵌套很多层, 或派生很多个cancelctx.

func newcancelctx(parent context) cancelctx {    return cancelctx{context: parent}  }

非暴露的构造函数.

回顾一下:cancelctx添加了context对取消信号的支持. 只要触发了"取消信号",使用方只需要监听done信道即可.

myctx mydonectx othercontext属于测试,等分析测试的时候再细说.

timerctx

前面说到了取消信号对应的上下文cancelctx,timerctx就是基于取消信号上下扩展的

type timerctx struct {    cancelctx    timer *time.timer      deadline time.time  }

注释说明:内嵌cancelctx是为了复用done和err,扩展了一个定时器和一个截至时间, 在定时器触发时触发cancelctx.cancel()即可.

  func (c *timerctx) deadline() (deadline time.time, ok bool) {        return c.deadline, true      }      func (c *timerctx) string() string {        return contextname(c.cancelctx.context) + ".withdeadline(" +          c.deadline.string() + " [" +          time.until(c.deadline).string() + "])"      }      func (c *timerctx) cancel(removefromparent bool, err error) {        c.cancelctx.cancel(false, err)        if removefromparent {          removechild(c.cancelctx.context, c)        }        c.mu.lock()        if c.timer != nil {          c.timer.stop()          c.timer = nil        }        c.mu.unlock()      }

timerctx内嵌了cancelctx,说明timerctx也实现了canceler接口, 从源码中可以看出,cancel()是重新实现了,string/deadline都重新实现了.

cancel()中额外添加了定时器的停止操作.

这里没有deadline设置和定时器timer开启的操作,会放在with-系列函数中.

回顾一下: context的deadline是机会取消信号实现的.

valuectx

valuectx和timerctx不同,是直接基于context的.

type valuectx struct {    context    key, val interface{}  }

一个valuectx附加了一个kv对.实现了valuestring.

   func stringify(v interface{}) string {        switch s := v.(type) {        case stringer:          return s.string()        case string:          return s        }        return ""      }      func (c *valuectx) string() string {        return contextname(c.context) + ".withvalue(type " +          reflectlite.typeof(c.key).string() +          ", val " + stringify(c.val) + ")"      }      func (c *valuectx) value(key interface{}) interface{} {        if c.key == key {          return c.val        }        return c.context.value(key)      }

因为valuectx.val类型是接口类型interface{},所以获取具体值时, 使用了switch type.

六、with-系列函数

支持取消信号 withcancel:

var canceled = errors.new("context canceled")  func withcancel(parent context) (ctx context, cancel cancelfunc) {    if parent == nil {      panic("cannot create context from nil parent")    }    c := newcancelctx(parent)    propagatecancel(parent, &c)    return &c, func() { c.cancel(true, canceled) }  }

派生一个支持取消信号的context,类型是cancelctx,cancelfunc是取消操作, 具体是调用cancelctx.cancel()函数,err参数是canceled.

    func propagatecancel(parent context, child canceler) {        done := parent.done()        if dOne== nil {          return // parent is never canceled        }          select {        case <-done:          // parent is already canceled          child.cancel(false, parent.err())          return        default:        }          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{})            }            p.children[child] = struct{}{}          }          p.mu.unlock()        } else {          atomic.addint32(&goroutines, +1)          go func() {            select {            case <-parent.done():              child.cancel(false, parent.err())            case <-child.done():            }          }()        }      }

传播取消信号.如果父context不支持取消信号,那就不传播. 如果父context的取消信号已经触发(就是父context的done信道已经触发或关闭), 之后判断父context是不是cancelctx,如果是就将此context丢到children中, 如果父context不是cancelctx,那就起协程监听父子context的done信道.

小技巧:

select {  case <-done:    child.cancel(false, parent.err())    return  default:  }

不加default,会等到done信道有动作;加了会立马判断done信道,done没操作就结束select.

select {  case <-parent.done():    child.cancel(false, parent.err())  case <-child.done():  }

这个会等待,因为没有加default.

因为顶层context目前只能是background和todo,不是cancelctx, 所以顶层context的直接派生context不会触发propagatecancel中的和children相关操作, 至少得3代及以后才有可能.

withcancel的取消操作会释放相关资源,所以在上下文操作完之后,最好尽快触发取消操作. 触发的方式是:done信道触发,要么有数据,要么被关闭.

支持截至日期 withdeadline:

 

   func withdeadline(parent context, d time.time) (context, cancelfunc) {        if parent == nil {          panic("cannot create context from nil parent")        }        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,        }        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) }      }

with-系列函数用于生成派生context,函数内部第一步都是判断父context是否为nil, withdeadline第二步是判断父context是否支持deadline,支持就将取消信号传递给派生context, 如果不支持,就为当前派生context支持deadline.

先理一下思路,目前context的实现类型有4个:emptyctx/cancelctx/timerctx/valuectx, 除了emptyctx,实现deadline()方法的只有timerctx,(ps:这里的实现特指有业务意义的实现), 唯一可以构造timerctx的只有withdeadline的第二步中. 这么说来,顶层context不支持deadline,最多第二层派生支持deadline的context, 第三层派生用于将取消信号进行传播.

withcancel上面已经分析了,派生一个支持取消信号的context,并将父context的取消信号 传播到派生context(ps:这么说有点绕,简单点讲就是将派生context添加到父context的children), 下面看看第一个构造支持deadline的过程.

构造timerctx,传播取消信号,判断截至日期是否已过,如果没过,利用time.afterfunc创建定时器, 设置定时触发的协程处理,之后返回派生context和取消函数.

可以看到,整个withdeadline是基于withcancel实现的,截至日期到期后,利用取消信号来做后续处理.

因为timerctx是内嵌了cancelctx,所以有一个派生context是可以同时支持取消和deadline的, 后面的value支持也是如此.

withdeadline的注释说明: 派生context的deadline不晚于参数,如果参数晚于父context支持的deadline,使用父context的deadline, 如果参数指定的比父context早,或是父context不支持deadline,那么派生context会构造一个新的timerctx. 父context的取消/派生context的取消/或者deadline的过期,都会触发取消信号对应的操作执行, 具体就是done()信道会被关闭.

func withtimeout(parent context,    timeout time.duration) (context, cancelfunc) {    return withdeadline(parent, time.now().add(timeout))  }

witchtimeout是基于withdeadline实现的,是一种扩展,从设计上可以不加,但加了会增加调用者的便捷. withtimeout可用在"慢操作"上.上下文使用完之后,应该立即调用取消操作来释放资源.

支持值witchvalue:

func withvalue(parent context, key, val interface{}) context {    if parent == nil {      panic("cannot create context from nil parent")    }    if key == nil {      panic("nil key")    }    if !reflectlite.typeof(key).comparable() {      panic("key is not comparable")    }    return &valuectx{parent, key, val}  }

只要是key能比较,就构造一个valuectx,用value()获取值时,如果和当前派生context的key不匹配, 就会和父context的key做匹配,如果不匹配,最后顶层context会返回nil.

总结一下:如果是value(),会一直通过派生context找到顶层context; 如果是deadline,会返回当前派生context的deadline,但会受到父context的deadline和取消影响; 如果是取消函数,会将传播取消信号的相关context都做取消操作. 最重要的是context是一个树形结构,可以组成很复杂的结构.

到目前为止,只了解了包的内部实现(顶层context的构造/with-系列函数的派生), 具体使用,需要看例子和实际测试.

ps:一个包内部如何复杂,对外暴露一定要简洁.一个包是无法设计完美的,但是约束可以, 当大家都接受一个包,并接受使用包的规则时,这个包就成功了,context就是典型.

对于值,可以用withvalue派生,用value取; 对于cancel/deadline,可以用withdeadline/withtimeout派生,通过done信号获取结束信号, 也可以手动用取消函数来触发取消操作.整个包的功能就这么简单.

七、扩展功能以及如何扩展

扩展功能现在支持取消/deadline/value,扩展这个层级不应该放在这个包, 扩展context,也就是新建context的实现类型,这个是可以的, 同样实现类型需要承载扩展功能,也不合适.

type canceler interface {    cancel(removefromparent bool, err error)    done() <-chan struct{}  }

接口canceler是保证取消信号可以在链上传播,cancel方法由cancelctx/timerctx实现, done只由cancelctx创建done信道,不管是从功能上还是方法上都没有扩展的必要.

剩下的就是value扩展成多kv对,这个主要还是要看应用场景.

八、补充

context被取消后err返回canceled错误,超时之后err返回deadlineexceeded错误, 这个deadlineexceeded还有些说法: 

 var deadlineexceeded error = deadlineexceedederror{}      type deadlineexceedederror struct{}    func (deadlineexceedederror) error() string {      return "context deadline exceeded"    }    func (deadlineexceedederror) timeout() bool   { return true }    func (deadlineexceedederror) temporary() bool { return true }

再看看net.error接口:

type error interface {    error    timeout() bool   // is the error a timeout?    temporary() bool // is the error temporary?  }

context中的deadlineexceeded默认是实现了net.error接口的实例. 这个是为后面走网络超时留下的扩展.

到此这篇关于对go语言中的context包源码分析的文章就介绍到这了,更多相关go语言context包源码分析内容请搜索<编程笔记>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<编程笔记>!

需要了解更多python教程分享对Go语言中的context包源码分析,都可以关注python教程分享栏目&#8212;编程笔记


推荐阅读
author-avatar
手机用户2502939421
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有