GO 中 defer的实现原理

GO 中 defer的实现原理

defer的实现原理.jpg

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

  • 分享了 GO 中通道是什么
  • 通道的底层数据结构详细解析
  • 通道在GO源码中是如何实现的
  • Chan 读写的基本原理
  • 关闭通道会出现哪些异常,panic
  • select 的简单应用

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

image

defer 是什么?

咱们一起来看看 defer 是个啥

是 GO 中的一个关键字

这个关键字,我们一般用在释放资源,在 return 前会调用他

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

image

遵循后进先出原则

后进入栈的,先出栈

先进入栈的,后出栈

  • 队列
image

遵循先进先出 , 我们就可以想象一个单向的管道,从左边进,右边出

先进来,先出去

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

defer 实现原理

image

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

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

具体的 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 说明
siz defer函数的参数和结果的内存大小
fn 需要被延迟执行的函数
_panic defer 的 panic 结构体
link 同一个协程里面的defer 延迟函数,会通过该指针连接在一起
heap 是否分配在堆上面
openDefer 是否经过开放编码优化
sp 栈指针(一般会对应到汇编)
pc 程序计数器

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

image

通过上述参数的描述,我们可以知道,defer的数据结构和函数类似,也是有如下三个参数:

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

可是我们是不是也发现了,成员里面还有一个link,同一个协程里面的defer 延迟函数,会通过该指针连接在一起

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

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

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

咱们来画个图形象一点

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

image

再声明 defer test2()

image

可以看出后声明的defer会插入到单链表的头,先声明的defer被排到后面去了

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

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

image-20210618144713620

源码文件在 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:nosplit
func 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:noescape
func 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
image

来我们看看 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:nosplit
func 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)
                }
image

点进去看池子的数据结构,其实里面的成员也就是 咱们之前说到的 _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 = siz
d.heap = true

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

image
// 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_*.s
func return0()

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

它在deferproc函数的最后被调用,用来通知调用Go的函数它不应该跳转到deferreturn

在正常情况下 return0 正常返回 0

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

简单说下 deferreturn

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

image

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,小伙伴们可以将代码拷贝下来,自己运行一波

image

第三点也来一个DEMO

延迟函数可能会影响到整个函数的返回值

func test3() (res int) {
   defer func() {
      res++
   }()

   return 1
}
func main() {

   fmt.Println(test3())

   return
}

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

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

res = 1

res++

因此,结果就是 2

image

总结

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

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

[图片上传失败...(image-9e3e71-1624153918664)]

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

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345