[TOC]

GO 中 defer的实现原理

咱们来回顾一下上次的分享,分享了对于 通道的一些知识点

  • 分享了 GO 中通道是什么
  • 通道的底层数据结构具体解析
  • 通道在GO源码中是如何实现的
  • Chan 读写的基本原理
  • 敞开通道会呈现哪些异样,panic
  • select 的简略利用

要是对 chan 通道还有点趣味的话,欢送查看文章 GO 中 Chan 实现原理分享

defer 是什么?

咱们一起来看看 defer 是个啥

是 GO 中的一个关键字

这个关键字,咱们个别用在开释资源,在 return 前会调用他

如果程序中有多个 defer ,defer 的调用程序是依照相似的形式,后进先出 LIFO的 ,这里顺便写一下

遵循后进先出准则

后进入栈的,先出栈

先进入栈的,后出栈

  • 队列

遵循先进先出 , 咱们就能够设想一个单向的管道,从右边进,左边出

先进来,先进来

后进来,后进来,不准插队

defer 实现原理

咱们先抛出一个论断,先心里有点底:

  • 代码中申明 defer的地位,编译的时候会插入一个函数叫做 deferproc ,在该defer所在的函数前插入一个返回的函数,不是return 哦,是deferreturn

具体的 defer 的实现原理是咋样的,咱们还是一样的,来看看 defer的底层数据结构是啥样的 ,

src/runtime/runtime2.gotype _defer struct {构造

// A _defer holds an entry on the list of deferred calls.// If you add a field here, add code to clear it in freedefer and deferProcStack// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct// and cmd/compile/internal/gc/ssa.go:(*state).call.// Some defers will be allocated on the stack and some on the heap.// All defers are logically part of the stack, so write barriers to// initialize them are not required. All defers must be manually scanned,// and for heap defers, marked.type _defer struct {   siz     int32 // includes both arguments and results   started bool   heap    bool   // openDefer indicates that this _defer is for a frame with open-coded   // defers. We have only one defer record for the entire frame (which may   // currently have 0, 1, or more defers active).   openDefer bool   sp        uintptr  // sp at time of defer   pc        uintptr  // pc at time of defer   fn        *funcval // can be nil for open-coded defers   _panic    *_panic  // panic that is running defer   link      *_defer   // If openDefer is true, the fields below record values about the stack   // frame and associated function that has the open-coded defer(s). sp   // above will be the sp for the frame, and pc will be address of the   // deferreturn call in the function.   fd   unsafe.Pointer // funcdata for the function associated with the frame   varp uintptr        // value of varp for the stack frame   // framepc is the current pc associated with the stack frame. Together,   // with sp above (which is the sp associated with the stack frame),   // framepc/sp can be used as pc/sp pair to continue a stack trace via   // gentraceback().   framepc uintptr}

_defer 持有提早调用列表中的一个条目 ,咱们来看看上述数据结构的参数都是啥意思

tag阐明
sizdefer函数的参数和后果的内存大小
fn须要被提早执行的函数
_panicdefer 的 panic 构造体
link同一个协程外面的defer 提早函数,会通过该指针连贯在一起
heap是否调配在堆下面
openDefer是否通过凋谢编码优化
sp栈指针(个别会对应到汇编)
pc程序计数器

defer 关键字前面必须是跟函数,这一点咱们要记住哦

通过上述参数的形容,咱们能够晓得,defer的数据结构和函数相似,也是有如下三个参数:

  • 栈指针 SP
  • 程序计数器 PC
  • 函数的地址

可是咱们是不是也发现了,成员外面还有一个link,同一个协程外面的defer 提早函数,会通过该指针连贯在一起

这个link指针,是指向的一个defer单链表的头,每次咱们申明一个defer的时候,就会将该defer的数据插入到这个单链表头部的地位,

那么,执行defer的时候,咱们是不是就能猜到defer 是咋获得了不?

后面有说到defer是后进先出的,这里当然也是遵循这个情理,取defer进行执行的时候,是从单链表的头开始去取的。

咱们来画个图形象一点

在协程A中申明2defer,先申明 defer test1()

再申明 defer test2()

能够看出后申明的defer会插入到单链表的头,先申明的defer被排到前面去了

咱们取的时候也是始终取头下来执行,直到单链表为空。

咱一起来看看defer 的具体实现

源码文件在 src/runtime/panic.go 中,查看 函数 deferproc

// Create a new deferred function fn with siz bytes of arguments.// The compiler turns a defer statement into a call to this.//go:nosplitfunc deferproc(siz int32, fn *funcval) { // arguments of fn follow fn   gp := getg()   if gp.m.curg != gp {      // go code on the system stack can't defer      throw("defer on system stack")   }   // the arguments of fn are in a perilous state. The stack map   // for deferproc does not describe them. So we can't let garbage   // collection or stack copying trigger until we've copied them out   // to somewhere safe. The memmove below does that.   // Until the copy completes, we can only call nosplit routines.   sp := getcallersp()   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)   callerpc := getcallerpc()   d := newdefer(siz)   if d._panic != nil {      throw("deferproc: d.panic != nil after newdefer")   }   d.link = gp._defer   gp._defer = d   d.fn = fn   d.pc = callerpc   d.sp = sp   switch siz {   case 0:      // Do nothing.   case sys.PtrSize:      *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))   default:      memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))   }   // deferproc returns 0 normally.   // a deferred func that stops a panic   // makes the deferproc return 1.   // the code the compiler generates always   // checks the return value and jumps to the   // end of the function if deferproc returns != 0.   return0()   // No code can go here - the C return register has   // been set and must not be clobbered.}

deferproc 的作用是:

创立一个新的递延函数 fn,参数为 siz 字节,编译器将一个提早语句转换为对this的调用

getcallersp()

失去deferproc之前的rsp寄存器的值,实现的形式所有平台都是一样的

//go:noescapefunc getcallersp() uintptr // implemented as an intrinsic on all platforms

callerpc := getcallerpc()

此处失去 rsp之后,存储在 callerpc 中 , 此处是为了调用 deferproc 的下一条指令

d := newdefer(siz)

d := newdefer(siz) 新建一个defer 的构造,后续的代码是在给defer 这个构造的成员赋值

咱看看 deferproc 的大体流程:

  • 获取 deferproc之前的rsp寄存器的值
  • 应用newdefer 调配一个 _defer 构造体对象,并且将他放到以后的 _defer 链表的头
  • 初始化_defer 的相干成员参数
  • return0

来咱们看看 newdefer的源码

源码文件在 src/runtime/panic.go 中,查看函数newdefer

// Allocate a Defer, usually using per-P pool.// Each defer must be released with freedefer.  The defer is not// added to any defer chain yet.//// This must not grow the stack because there may be a frame without// stack map information when this is called.////go:nosplitfunc newdefer(siz int32) *_defer {    var d *_defer    sc := deferclass(uintptr(siz))    gp := getg()    if sc < uintptr(len(p{}.deferpool)) {        pp := gp.m.p.ptr()        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {            // Take the slow path on the system stack so            // we don't grow newdefer's stack.            systemstack(func() {                lock(&sched.deferlock)                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {                    d := sched.deferpool[sc]                    sched.deferpool[sc] = d.link                    d.link = nil                    pp.deferpool[sc] = append(pp.deferpool[sc], d)                }                unlock(&sched.deferlock)            })        }        if n := len(pp.deferpool[sc]); n > 0 {            d = pp.deferpool[sc][n-1]            pp.deferpool[sc][n-1] = nil            pp.deferpool[sc] = pp.deferpool[sc][:n-1]        }    }    if d == nil {        // Allocate new defer+args.        systemstack(func() {            total := roundupsize(totaldefersize(uintptr(siz)))            d = (*_defer)(mallocgc(total, deferType, true))        })    }    d.siz = siz    d.heap = true    return d}

newderfer 的作用:

通常应用per-P池,调配一个Defer

每个defer能够自在的开释。以后defer 也不会退出任何一个 defer链条中

getg()

获取以后协程的构造体指针

// getg returns the pointer to the current g.// The compiler rewrites calls to this function into instructions// that fetch the g directly (from TLS or from the dedicated register).func getg() *g

pp := gp.m.p.ptr()

拿到当前工作线程外面的 P

而后拿到 从全局的对象池子中拿一部分对象给到P的池子外面

for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {                    d := sched.deferpool[sc]                    sched.deferpool[sc] = d.link                    d.link = nil                    pp.deferpool[sc] = append(pp.deferpool[sc], d)                }

点进去看池子的数据结构,其实外面的成员也就是 咱们之前说到的 _defer指针

其中 sched.deferpool[sc] 是全局的池子,pp.deferpool[sc] 是本地的池子

mallocgc调配空间

上述操作若 d 没有拿到值,那么就间接应用 mallocgc 重新分配,且设置好 对应的成员 sizheap

if d == nil {        // Allocate new defer+args.        systemstack(func() {            total := roundupsize(totaldefersize(uintptr(siz)))            d = (*_defer)(mallocgc(total, deferType, true))        })    }d.siz = sizd.heap = true

mallocgc 具体实现在 src/runtime/malloc.go 中,若感兴趣的话,能够深刻看看这一块,明天咱们不重点说这个函数

// Allocate an object of size bytes.// Small objects are allocated from the per-P cache's free lists.// Large objects (> 32 kB) are allocated straight from the heap.func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}

最初再来看看return0

最初再来看看 deferproc 函数中的 后果返回return0()

// return0 is a stub used to return 0 from deferproc.// It is called at the very end of deferproc to signal// the calling Go function that it should not jump// to deferreturn.// in asm_*.sfunc return0()

return0 是用于从deferproc返回0的存根

它在deferproc函数的最初被调用,用来告诉调用Go的函数它不应该跳转到deferreturn

在失常状况下 return0 失常返回 0

可是异常情况下 return0 函数会返回 1,此时GO 就会跳转到执行 deferreturn

简略说下 deferreturn

deferreturn的作用就是状况defer外面的链表,偿还相应的缓冲区,或者把对应的空间让GC回收调

GO 中 defer 的规定

下面剖析了GO 中defer 的实现原理之后,咱们当初来理解一下 GO 中利用defer 是须要恪守 3 个规定的,咱们来列一下:

  • defer前面跟的函数,叫提早函数,函数中的参数在defer语句申明的时候,就曾经确定下来了
  • 提早函数的执行时依照后进先出来的,文章后面也屡次说到过,这个印象应该很粗浅吧,先呈现的defer后执行,后呈现的defer先执行
  • 提早函数可能会影响到整个函数的返回值

咱们还是要来解释一下的,下面第 2 点,应该都好了解,下面的图也表明了 执行程序

第一点咱们来写个小DEMO

提早函数中的参数在defer语句申明的时候,就曾经确定下来了

func main() {   num := 1   defer fmt.Println(num)   num++   return}

别猜了,运行后果是 1,小伙伴们能够将代码拷贝下来,本人运行一波

第三点也来一个DEMO

提早函数可能会影响到整个函数的返回值

func test3() (res int) {   defer func() {      res++   }()   return 1}func main() {   fmt.Println(test3())   return}

上述代码,咱们在 test3函数中的返回值,咱们提前命名好了,原本应该是返回后果为 1

可是在return 这里,执行程序这样的

res = 1

res++

因而,后果就是 2

总结

  • 分享了defer是什么
  • 简略示意了栈和队列
  • defer的数据结构和实现原理,具体的源码展现
  • GO中defer的 3 条规定

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

好了,本次就到这里,下一次 咱们用GO玩一下验证码

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

我是小魔童哪吒,欢送点赞关注珍藏,下次见~