使用的go版本为 go1.21.2
首先我们写一个简单的defer调度代码
package mainimport "fmt"func main() {defer func() {fmt.Println("xiaochuan")}()
}
通过go build -gcflags -S main.go获取到对应的汇编代码
可以在图中看到有个CALL runtime.deferreturn(SB) 调度这个是编译器插入的defer执行调度
我们先来看一下defer构造体的底层源码
defer结构体
//代码在GOROOT/src/runtime/runtime2.go中type _defer struct {started bool // 表示是否已经开始执行heap bool // 标志是否分配在堆上openDefer bool // 标志是否对应于一个带有 open-coded defers 的栈帧sp uintptr // 执行时的栈指针pc uintptr // 执行时的程序计数器fn func() // 存储被延迟执行的函数_panic *_panic // 当前执行的 panic(如果有的话)link *_defer // 在G(goroutine)上指向下一个延迟结构,可以指向堆或栈// 如果 openDefer 为 true,则以下字段记录与具有 open-coded defers 的栈帧相关的值。// 在这种情况下,sp 字段上面的 sp 是栈帧的 sp,而 pc 是关联函数中 deferreturn 调用的地址。fd unsafe.Pointer // 与栈帧相关的函数的 funcdatavarp uintptr // 栈帧的 varp 值framepc uintptr // 栈帧关联的当前 pc
}
deferreturn源码与解读
//代码在GOROOT/src/runtime/panic.go中
func deferreturn() {gp := getg() //获取当前运行Gfor {//逐步获取当前G中的defer调用d := gp._defer// 如果获取到的构造体为空,直接返回。if d == nil {return}// 获取调用 defer 语句的函数的栈指针。sp := getcallersp()// 如果_defer里面存的栈指针与当前函数的栈指针不匹配,直接返回。// 说明数据存在改写不给予处理if d.sp != sp {return}// 如果_defer使用了 open-coded defers(编码的延迟调用)if d.openDefer {// 运行 open-coded defers 的帧。done := runOpenDeferFrame(d)// 如果 open-coded defers 没有完成,抛出异常。if !done {throw("unfinished open-coded defers in deferreturn")}// 将_defer从G的延迟链表移除,释放对应的_defer构造体资源gp._defer = d.linkfreedefer(d)return}// 获取_defer中保存的执行函数fn := d.fnd.fn = nil// 从G中移除当前_defer,释放其资源。gp._defer = d.linkfreedefer(d)// 执行延迟函数。fn()}
}
freedefer源码与解读
//代码在GOROOT/src/runtime/panic.go中
func freedefer(d *_defer) {// _defer 结构的 link 字段设置为 nild.link = nil// 如果还存在_panic字段,调用 freedeferpanic 函数if d._panic != nil {freedeferpanic()}// 如果调度函数不为 nil,调用 freedeferfn 函数if d.fn != nil {freedeferfn()}// 如果不在堆上,直接返回if !d.heap {return}// 通过当前G的m字段去拿到对应的Mmp := acquirem()// 获取与M绑定的Ppp := mp.p.ptr()// 如果P中的本地缓存已满// 将一半的defer池放入到调度器中去// 调度器相当于全局池,具体使用是有锁,所以优先使用本地池if len(pp.deferpool) == cap(pp.deferpool) {var first, last *_deferfor len(pp.deferpool) > cap(pp.deferpool)/2 {n := len(pp.deferpool)d := pp.deferpool[n-1]pp.deferpool[n-1] = nilpp.deferpool = pp.deferpool[:n-1]if first == nil {first = d} else {last.link = d}last = d}// 获取调度器中的defer锁lock(&sched.deferlock)//放入到全局池last.link = sched.deferpoolsched.deferpool = first//释放调度器中的defer锁unlock(&sched.deferlock)}// 将 _defer 结构清零*d = _defer{}// 将 _defer 结构放回P的本地缓存pp.deferpool = append(pp.deferpool, d)// 释放 Mreleasem(mp)mp, pp = nil, nil
}
看老的版本的一些文章介绍在使用 defer func(){}() 时编译器会将转换为runtime.deferproc Go新版本没看到汇编对应的调度过程,希望有大哥能帮忙解答一下新版本是如何调度到runtime.deferproc函数
deferproc源码与解读
//代码在GOROOT/src/runtime/panic.go中
func deferproc(fn func()) {// 获取当前Ggp := getg()// 检查G是否在系统栈上if gp.m.curg != gp {// 系统栈上的 Go 代码不能使用 deferthrow("defer on system stack")}// 创建一个新的 defer 结构d := newdefer()// 检查新创建的 defer 结构的 _panic 字段是否为 nilif d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}// 将新的defer结构添加到当前G的defer链表中d.link = gp._defergp._defer = d// 设置defer触发函数d.fn = fn//GOROOT/src/runtime/stubs.go//注释是这么说的返回其调用者的调用者的程序计数器//具体实现在汇编层d.pc = getcallerpc()//GOROOT/src/runtime/stubs.go//注释是这么说的返回其调用者的调用者的堆栈指针//具体实现在汇编层d.sp = getcallersp()//GOROOT/src/runtime/stubs.go//return0 是一个用于从 deferproc 返回 0 的存根。//它在 deferproc 的最后调用来发出信号//调用 Go 函数时不应跳转//推迟返回。//具体实现在汇编层return0()// 不能在这里放置代码 - C 返回寄存器已设置,不能被破坏。
}
newdefer源码与解读
//代码在GOROOT/src/runtime/panic.go中
func newdefer() *_defer {// 声明一个_defer指针变量var d *_defer// 通过当前G的m字段去拿到对应的Mmp := acquirem()// 获取与M绑定的Ppp := mp.p.ptr()// 检查P中 deferpool 是否为空,且调度器中有可用的 defer 结构体if len(pp.deferpool) == 0 && sched.deferpool != nil {// 获取调度器中的defer锁lock(&sched.deferlock)// 将调度器中的deferpool转移到P的本地池中去for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {d := sched.deferpoolsched.deferpool = d.linkd.link = nilpp.deferpool = append(pp.deferpool, d)}// 释放调度器中的defer锁unlock(&sched.deferlock)}// 检查P的本地池中是否有可用的defer结构体if n := len(pp.deferpool); n > 0 {// 从本地池拿出来一个 defer 结构体d = pp.deferpool[n-1]pp.deferpool[n-1] = nilpp.deferpool = pp.deferpool[:n-1]}// 释放 Mreleasem(mp)mp, pp = nil, nil// 如果没有找到可用的 defer 结构体,则分配一个新的if d == nil {d = new(_defer)}// 将 'heap' 字段设置为 true 并返回 defer 结构体d.heap = truereturn d
}
总结
从上面的源码我们可以了解到defer的大致逻辑,当使用defer关键词时,会将当前要延迟的函数加入到G的延迟链表中去,当我们的函数执行完成后会触发deferreturn调度将G中的延迟链表循环执行一遍,来达到延迟执行的目的