使用的go版本为 go1.21.2
首先我们写一个简单的defer调度代码
package main
import "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 // 与栈帧相关的函数的 funcdata
varp uintptr // 栈帧的 varp 值
framepc uintptr // 栈帧关联的当前 pc
}
deferreturn源码与解读
//代码在GOROOT/src/runtime/panic.go中
func deferreturn() {
gp := getg() //获取当前运行G
for {
//逐步获取当前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.link
freedefer(d)
return
}
// 获取_defer中保存的执行函数
fn := d.fn
d.fn = nil
// 从G中移除当前_defer,释放其资源。
gp._defer = d.link
freedefer(d)
// 执行延迟函数。
fn()
}
}
freedefer源码与解读
//代码在GOROOT/src/runtime/panic.go中
func freedefer(d *_defer) {
// _defer 结构的 link 字段设置为 nil
d.link = nil
// 如果还存在_panic字段,调用 freedeferpanic 函数
if d._panic != nil {
freedeferpanic()
}
// 如果调度函数不为 nil,调用 freedeferfn 函数
if d.fn != nil {
freedeferfn()
}
// 如果不在堆上,直接返回
if !d.heap {
return
}
// 通过当前G的m字段去拿到对应的M
mp := acquirem()
// 获取与M绑定的P
pp := mp.p.ptr()
// 如果P中的本地缓存已满
// 将一半的defer池放入到调度器中去
// 调度器相当于全局池,具体使用是有锁,所以优先使用本地池
if len(pp.deferpool) == cap(pp.deferpool) {
var first, last *_defer
for len(pp.deferpool) > cap(pp.deferpool)/2 {
n := len(pp.deferpool)
d := pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
// 获取调度器中的defer锁
lock(&sched.deferlock)
//放入到全局池
last.link = sched.deferpool
sched.deferpool = first
//释放调度器中的defer锁
unlock(&sched.deferlock)
}
// 将 _defer 结构清零
*d = _defer{}
// 将 _defer 结构放回P的本地缓存
pp.deferpool = append(pp.deferpool, d)
// 释放 M
releasem(mp)
mp, pp = nil, nil
}
看老的版本的一些文章介绍在使用 defer func(){}() 时编译器会将转换为runtime.deferproc Go新版本没看到汇编对应的调度过程,希望有大哥能帮忙解答一下新版本是如何调度到runtime.deferproc函数
deferproc源码与解读
//代码在GOROOT/src/runtime/panic.go中
func deferproc(fn func()) {
// 获取当前G
gp := getg()
// 检查G是否在系统栈上
if gp.m.curg != gp {
// 系统栈上的 Go 代码不能使用 defer
throw("defer on system stack")
}
// 创建一个新的 defer 结构
d := newdefer()
// 检查新创建的 defer 结构的 _panic 字段是否为 nil
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
// 将新的defer结构添加到当前G的defer链表中
d.link = gp._defer
gp._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字段去拿到对应的M
mp := acquirem()
// 获取与M绑定的P
pp := 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.deferpool
sched.deferpool = d.link
d.link = nil
pp.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] = nil
pp.deferpool = pp.deferpool[:n-1]
}
// 释放 M
releasem(mp)
mp, pp = nil, nil
// 如果没有找到可用的 defer 结构体,则分配一个新的
if d == nil {
d = new(_defer)
}
// 将 'heap' 字段设置为 true 并返回 defer 结构体
d.heap = true
return d
}
总结
从上面的源码我们可以了解到defer的大致逻辑,当使用defer关键词时,会将当前要延迟的函数加入到G的延迟链表中去,当我们的函数执行完成后会触发deferreturn调度将G中的延迟链表循环执行一遍,来达到延迟执行的目的