defer函数的实践和原理

defer是什么

defer是Go语言提供的一种用于注册延迟调用的关键字。函数或语句可以在当前函数执行完成或者异常退出后执行。通常用于关闭文件描述符、释放锁、关闭连接以及解锁资源等场景。

defer实践

defer函数

注册defer函数分两种:

  1. 需要传参数的函数
defer func(a int) {
	//todo...
}(a)
defer process(b)
func process(b int){}
  1. 不需要传参数,但是又用到了外界的变量的函数
defer func() {
	//todo...
}()
defer process()
func process(){}

注册defer函数

//注册匿名函数
defer func() {
	//todo...
}()
//注册函数
defer textFun()

注意:defer函数不管是有名函数还是匿名函数,如果传入了参数(参数被复制了一份),则这个defer函数立即先求值。如果没有传参数,则这个函数依赖的外界变量等待外层函数执行完成后的值确定后再求值。

//打印函数执行时间
startedAt := time.Now()
//defer fmt.Println(time.Since(startedAt))//输出0s,外层函数执行到这一句的时候已经算好了值为0
defer func() {
    fmt.Println(time.Since(startedAt))//这个闭包依赖外层变量startedAt,需要等函数执行完成后确定了startedAt的值再执行求值
}()
time.Sleep(time*Second*3)

注意:return或者panic后的defer函数是不会注册到defer函数栈的,故不会被延迟执行。

执行顺序

外层函数执行时,延迟语句会被压入栈中,当外层函数返回或者退出时,defer函数会按照入栈的顺序出栈执行(先进后出)。
原因:后面定义的函数可能会依赖前面的资源,当然需要先执行,如果前面先执行了,后面函数依赖的资源没有了或者发生了改变。

函数结束时执行流程

renturn/panic 把返回值复制给返回变量–>出栈顺序调用defer函数–>空的returm

defer应用场景

关闭资源

f,ok := os.Open("a.txt")
if ok != nil {
    log.Fater("error")
}
defer f.Close()

捕获异常

	defer func() {
		if r := recover(); r != nil {
			log.Default()
		}
	}()

注意:recover()函数只能在defer函数里面使用。panic后不影响主进程继续运行。

底层原理

本节内容借鉴了https://mp.weixin.qq.com/s/iEtMbRXW4yYyCG0TTW5y9g

核心流程:编译器会把 defer 语句翻译成对 deferproc 函数的调用,同时,编译器也会在使用了 defer 语句的 go 函数的末尾插入对 deferreturn 函数的调用。

先放一个例子:

package main

import "fmt"

func sum(a, b int) {
    c := a + b
    fmt.Println("sum:" , c)
}

func f(a, b int) {
    defer sum(a, b)

    fmt.Printf("a: %d, b: %d\n", a, b)
}

func main() {
    a, b := 1, 2
    f(a, b)
}

deferproc 函数

函数说明
func deferproc(siz int32, fn *funcval)
type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

说明:

  1. 形参siz:defered 函数(比如本例中的 sum 函数)的参数以字节为单位的大小
  2. fn:funcval结构体变量(比如本例中funcval{fn:sum})的指针
  3. 为什么需要把 sum 函数的参数大小传递给 deferproc() 函数?另外为什么没看到 sum 函数需要的两个参数呢?

答:在调用 runtime.deferproc 时,栈上除了保存了 deferproc 函数需要的两个参数之外,还保存了 defered 函数所需要的参数(比如本例中的 sum 函数,它的两个参数 a 和 b 也都保存在了栈上,它们紧邻 deferproc 函数的第二个参数),也就是说,在执行 defer 语句时,defer 后面的函数的参数已经确定了。另外需要注意的是,从 deferproc 函数的原型可以知道它并没有返回值,但上面的汇编代码在调用了 deferproc 函数之后却检查了 rax 寄存器的值是否为0(0x0000000000488e36 <+86>: test %eax,%eax),也就是说 deferproc 函数实际上会通过 rax 寄存器返回一个隐性的返回值。

函数执行流程
// 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
    if getg().m.curg != getg() {  //用户goroutine才能使用defer
        // 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.
 
    // 对getcallersp()和getcallerpc() 函数的分析可以参考本公众号的其它文章
    sp := getcallersp() //sp = 调用deferproc之前的rsp寄存器的值
    // argp指向defer函数的第一个参数,本例为sum函数的参数a
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc() // deferproc函数的返回地址

    d := newdefer(siz)
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }
    d.fn = fn //需要延迟执行的函数
    d.pc = callerpc  //记录deferproc函数的返回地址,主要用于panic/recover
    d.sp = sp //调用deferproc之前rsp寄存器的值
 
    //把defer函数需要用到的参数拷贝到d结构体后面,下面的deferrArgs返回的是一个地址
    //deferArgs(d) = d + sizeof(d) ,newdefer返回的内存空间 >= deferArgs(d)
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize: //如果defered函数的参数只有指针大小则直接通过赋值来拷贝参数
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default: //通过memmove拷贝defered函数的参数
        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()  //通过汇编指令设置rax = 0
    // No code can go here - the C return register has
    // been set and must not be clobbered.
}
过程分析
  1. 它首先通过 newdefer 函数分配一个 _defer 结构体对象,如何分配的呢?

答:newdefer 函数首先会尝试从与当前工作线程M绑定的 P(逻辑处理器) 的 _defer 对象池和全局对象池中获取一个满足大小要求(sizeof(_defer) + siz向上取整至16的倍数)的 _defer 结构体对象,如果没有能够满足要求的空闲 _defer 对象则从堆上分一个,最后把分配到的对象加入当前 goroutine 的 _defer 链表的表头。

  1. 然后把需要延迟执行的函数以及该函数需要用到的参数、调用 deferproc 函数时的 rsp 寄存器的值以及 deferproc 函数的返回地址,都保存在 _defer 结构体对象之中,最后通过 return0() 设置 rax 寄存器的值为 0 隐性的给调用者返回一个 0 值。
  2. deferproc 明明只会隐性的返回 0 值,但为什么上面的 f() 函数在调用了 deferproc 之后还用了一条指令来判断返回值是否是 0 呢,这不多此一举吗?

答:事实上这里主要与 panic 和 recover 的实现机制有关,当程序发生 panic 之后,程序会“再次从 deferproc 函数返回”,这种情况下返回值就不是 0 了。

  1. _defer结构体分析:
type _defer struct {
	siz       int32//是参数和结果的内存大小
	started   bool//该 defer 是否已经执行过
    heap//表明该defer是否存储在heap上
	openDefer bool//表示当前 defer 是否经过开放编码的优化
	sp        uintptr//栈指针
	pc        uintptr//调用方的程序计数器-defer语句下一条语句的地址
	fn        *funcval//defer 关键字中传入的函数
	_panic    *_panic//是触发延迟调用的结构体,可能为空
	link      *_defer//指向 _defer 链表。runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。
}

对于本文的例子,初始化完成后的 _defer 结构体对象各成员的值大致如下:

d.siz = 16
d.started = false
d.sp = 调用deferproc函数之前的rsp寄存器的值
d.pc = 0x0000000000488e36
d.fn = &funcval{sum}
d._panic = nil
d._defer = nil
sum函数的参数a//defered 函数的参数紧跟其后
sum函数的参数b//defered 函数的参数紧跟其后

注意:defered 函数的参数并未在 _defer 结构体中定义,它所需要的参数在内存中紧跟在 _defer 结构体对象的后面。

总结概述

defer 语句中被延迟执行的函数挂入当前 goroutine 的 _defer 链表的流程总结:

  1. 编译器会把 go 代码中 defer 语句翻译成对 deferproc 函数的调用;
  2. deferproc 函数通过 newdefer 函数分配一个 _defer 结构体对象并放入当前 goroutine 的 _defer 链表的表头;
  3. 在 _defer 结构体对象中保存被延迟执行的函数 fn 的地址以及 fn 所需的参数;
  4. 返回到调用 deferproc 的函数继续执行后面的代码。

deferreturn 函数

func deferreturn(arg0 uintptr) {
    gp := getg()  //获取当前goroutine对应的g结构体对象
    d := gp._defer //defer函数链表
    if d == nil {
        //没有需要执行的函数直接返回,deferreturn和deferproc是配对使用的
        //为什么这里d可能为nil?因为deferreturn其实是一个递归调用,这个是递归结束条件之一
        return
    }
    sp := getcallersp()  //获取调用deferreturn时的栈顶位置
    if d.sp != sp {  //递归结束条件之二
        //如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回
       //因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如:
       //a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数
        return
    }

    // Moving arguments around.
    //
    // Everything called after this point must be recursively
    // nosplit because the garbage collector won't know the form
    // of the arguments until the jmpdefer can flip the PC over to
    // fn.
   //把保存在_defer对象中的fn函数需要用到的参数拷贝到栈上,准备调用fn
   //注意fn的参数放在了调用调用者的栈帧中,而不是此函数的栈帧中
    switch d.siz {
        case 0:
            // Do nothing.
        case sys.PtrSize:
            *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
        default:
            memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link //使gp._defer指向下一个_defer结构体对象
   //因为需要调用的函数d.fn已经保存在了fn变量中,它的参数也已经拷贝到了栈上,所以释放_defer结构体对象
    freedefer(d)
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))  //调用fn
}
主要流程
  1. 通过当前 goroutine 对应的 g 结构体对象的 _defer 链表判断是否有需要执行的 defered 函数,如果没有(g._defer == nil 或则 defered 函数不是在 deferreturn 的 caller 函数中注册的函数)则直接返回;
  2. 从 _defer 对象中把 defered 函数需要的参数拷贝到栈上;
  3. 释放 _defer 结构体对象;
  4. 通过 jmpdefer 函数调用 defered 函数(比如本文的sum函数)。

注意:
(1)d == nil 和 d.sp != sp。其中 d == nil 在判断是否有 defered 函数需要执行,可能有些读者会有疑问,deferreturn 明明是与 deferproc 配套使用的,这里怎么会是nil呢?这个是因为deferreturn 函数其实是被递归调用的,每次调用它只会执行一个 defered 函数,比如本文使用的例子在 f() 函数中注册了一个 defered 函数(sum函数),所以 deferreturn 函数会被调用两次,第一次进入时会去执行 sum 函数,第二次进入时 d 为 nil 就直接返回了;另外一个条件 d.sp != sp 在判断 d 对象所包装的 defered 函数现在是否应该被执行,比如有函数调用链a()->b()->c(),即 a 函数调用了 b 函数,b 函数又调用了 c 函数,它们都通过 defer 注册了延迟函数,那么当 c() 执行完时只能执行在 c 中注册的函数,而不能执行 a 函数和 b 函数注册的 defered 函数;

jmpdefer 函数

函数说明
// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
    MOVQ  fv+0(FP), DX  # fn,fn.fn = sum函数的地址
    MOVQ  argp+8(FP), BX  # caller sp
    LEAQ  -8(BX), SP  # caller sp after CALL
    MOVQ  -8(SP), BP  # restore BP as if deferreturn returned (harmless if framepointers not in use)
    SUBQ  $5, (SP)  # return to CALL again
    MOVQ  0(DX), BX 
    JMP  BX  # but first run the deferred function

第一步:

MOVQ  fv+0(FP), DX  # fn,fn.fn = sum函数的地址

把jmpdefer的第一个参数也就是结构体对象fn的地址放入DX寄存器,之后的代码就可以通过DX寄存器访问到fn.fn从而拿到 sum 函数的地址。
第二步:

MOVQ  argp+8(FP), BX  # caller sp

把jmpdefer的第二个参数放入 BX 寄存器,该参数是一个指针,它指向 sum 函数的第一个参数。
第三步:

LEAQ  -8(BX), SP  # caller sp after CALL

BX 存放的是一个指针,BX - 8所指的位置是 deferreturn 函数执行完后的返回地址 0x488ef0,所以这条指令的作用是让 SP 寄存器指向 deferreturn 函数的返回地址所在的栈内存单元。
第四步:

MOVQ  -8(SP), BP  # restore BP as if deferreturn returned (harmless if framepointers not in use)

调整 BP 寄存器的值,因为此时 SP - 8 的位置存放的是 f() 函数的 rbp 寄存器的值,所以这条指令在调整 rbp 寄存器的值使其指向 f() 函数的栈帧的适当位置。
第五步:

SUBQ  $5, (SP)  # return to CALL again

CPU 在执行这条指令是,rsp 寄存器指向的是 deferreturn 函数的返回地址,也就是 f() 函数中的 0x0000000000488ef0 <+272>: mov 0x78(%rsp),%rbp 这一条指令的地址,即0x488ef0,所以这条指令把 rsp 寄存器所指的内存中的值 0x488ef0 减了 5 得到 0x488eeb,对照前面f函数的汇编代码可知,这个地址指向的是 0x0000000000488eeb <+267>: callq 0x427490 <runtime.deferreturn> 这条指令。
第六~七条指令:

MOVQ  0(DX), BX       # BX = fn.fn
JMP  BX  # but first run the deferred function

会跳转到 sum 函数去执行,完成对 sum 函数的调用。因为 sum 函数的返回地址被上面的第5条指令设置成了 0x488eeb,所以等 sum 函数执行完成之后它会直接返回到 f() 函数0x0000000000488eeb <+267>: callq 0x427490 <runtime.deferreturn>指令处继续执行,而这条指令又调用了 deferreturn 函数。

总结:
defer函数执行流程:外层函数()->deferproc()->deferreturn()->jmpdefer()->被defer函数()–>deferreturn()->jmpdefer()->被defer函数()->…

总结

func x() {
     .......
     defer y(......)
     .......
}

首先:编译器会把 defer 语句翻译成对 deferproc 函数的调用,deferproc 负责构造一个用来保存 y 函数的地址以及 y 函数需要用到的参数的 _defer 结构体对象,并把该对象加入当前 goroutine 对应的 g 结构体对象的 _defer 链表表头;
然后:编译器会在 x 函数的结尾处插入对 deferreturn 的调用,deferreturn 负责递归的调用 x 函数通过 defer 语句注册的函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值