共计 7421 个字符,预计需要花费 19 分钟才能阅读完成。
[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.go
的 type _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 关键字前面必须是跟函数,这一点咱们要记住哦
通过上述参数的形容,咱们能够晓得,defer
的数据结构和函数相似,也是有如下三个参数:
- 栈指针 SP
- 程序计数器 PC
- 函数的地址
可是咱们是不是也发现了,成员外面还有一个link
,同一个协程外面的 defer 提早函数,会通过该指针连贯在一起
这个 link
指针,是指向的一个 defer
单链表的头,每次咱们申明一个 defer
的时候,就会将该 defer
的数据插入到这个单链表头部的地位,
那么,执行 defer
的时候,咱们是不是就能猜到defer
是咋获得了不?
后面有说到 defer
是后进先出的,这里当然也是遵循这个情理,取 defer
进行执行的时候,是从单链表的头开始去取的。
咱们来画个图形象一点
在协程 A 中申明 2 个defer
,先申明 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: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
来咱们看看 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)
}
点进去看池子的数据结构,其实外面的成员也就是 咱们之前说到的 _defer
指针
其中 sched.deferpool[sc]
是全局的池子,pp.deferpool[sc]
是本地的池子
mallocgc调配空间
上述操作若 d 没有拿到值,那么就间接应用 mallocgc
重新分配,且设置好 对应的成员 siz
和 heap
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
中,若感兴趣的话,能够深刻看看这一块,明天咱们不重点说这个函数
// 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
回收调
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 玩一下验证码
技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。
我是 小魔童哪吒,欢送点赞关注珍藏,下次见~