简介
延迟调用(defer)的优势是:
- 即使函数执行出错,依然能保证回收资源等操作得以执行
- 可以在变量的定义处加入defer,代码结构上避免忘记做某些数据的回收
劣势:
- 性能上会会比直接调用慢一些
- 如果在defer中释放,相对来说只会在函数执行结束的时候才会调用,变量生命周期会变长.
定义
编写以下程序, dump出汇编.
defer
主要调用了一下两个函数func deferprocStack(d *_defer)
和func deferreturn(arg0 uintptr)
package main
import (
"fmt"
)
func main() {
defer fmt.Println(0x11)
}
(base) ➜ readsrc go tool objdump -s "main\.main" ./test
TEXT main.main(SB) /home/darcyaf/Development/go/src/readsrc/main.go
main.go:7 0x48cf30 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
main.go:7 0x48cf39 488d4424d8 LEAQ -0x28(SP), AX
main.go:7 0x48cf3e 483b4110 CMPQ 0x10(CX), AX
main.go:7 0x48cf42 0f86b1000000 JBE 0x48cff9
main.go:7 0x48cf48 4881eca8000000 SUBQ $0xa8, SP
main.go:7 0x48cf4f 4889ac24a0000000 MOVQ BP, 0xa0(SP)
main.go:7 0x48cf57 488dac24a0000000 LEAQ 0xa0(SP), BP
main.go:8 0x48cf5f 0f57c0 XORPS X0, X0
main.go:8 0x48cf62 0f11842490000000 MOVUPS X0, 0x90(SP)
main.go:8 0x48cf6a 488d050f190100 LEAQ 0x1190f(IP), AX
main.go:8 0x48cf71 4889842490000000 MOVQ AX, 0x90(SP)
main.go:8 0x48cf79 488d05a0cd0400 LEAQ 0x4cda0(IP), AX
main.go:8 0x48cf80 4889842498000000 MOVQ AX, 0x98(SP)
main.go:8 0x48cf88 c744243030000000 MOVL $0x30, 0x30(SP)
main.go:8 0x48cf90 488d0561c80300 LEAQ 0x3c861(IP), AX
main.go:8 0x48cf97 4889442448 MOVQ AX, 0x48(SP)
main.go:8 0x48cf9c 488d842490000000 LEAQ 0x90(SP), AX
main.go:8 0x48cfa4 4889442460 MOVQ AX, 0x60(SP)
main.go:8 0x48cfa9 48c744246801000000 MOVQ $0x1, 0x68(SP)
main.go:8 0x48cfb2 48c744247001000000 MOVQ $0x1, 0x70(SP)
main.go:8 0x48cfbb 488d442430 LEAQ 0x30(SP), AX
main.go:8 0x48cfc0 48890424 MOVQ AX, 0(SP)
main.go:8 0x48cfc4 e867b7f9ff CALL runtime.deferprocStack(SB)
main.go:8 0x48cfc9 85c0 TESTL AX, AX
main.go:8 0x48cfcb 7516 JNE 0x48cfe3
main.go:9 0x48cfcd 90 NOPL
main.go:9 0x48cfce e85dbdf9ff CALL runtime.deferreturn(SB)
main.go:9 0x48cfd3 488bac24a0000000 MOVQ 0xa0(SP), BP
main.go:9 0x48cfdb 4881c4a8000000 ADDQ $0xa8, SP
main.go:9 0x48cfe2 c3 RET
main.go:8 0x48cfe3 90 NOPL
main.go:8 0x48cfe4 e847bdf9ff CALL runtime.deferreturn(SB)
main.go:8 0x48cfe9 488bac24a0000000 MOVQ 0xa0(SP), BP
main.go:8 0x48cff1 4881c4a8000000 ADDQ $0xa8, SP
main.go:8 0x48cff8 c3 RET
main.go:7 0x48cff9 e8a247fcff CALL runtime.morestack_noctxt(SB)
main.go:7 0x48cffe e92dffffff JMP main.main(SB)
在func deferprocStack(d *_defer)
, 这里将defer的函数调用全部放到g._defer上,串成一个链表等待调用,将新加入的defer调用放在前面,然后根据link去调用,这也能解释为什么越晚的defer越早调用.
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false
d.sp = getcallersp()
d.pc = getcallerpc()
// The lines below implement:
// d.panic = nil
// d.link = gp._defer
// gp._defer = d
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
(dlv) p gp._defer
*runtime._defer {
siz: 48,
started: false,
heap: false,
sp: 824634305936,
pc: 4771785,
fn: *runtime.funcval {fn: 4745216},
_panic: *runtime._panic nil,
link: *runtime._defer {
siz: 48,
started: false,
heap: false,
sp: 824634306112,
pc: 4772144,
fn: *(*runtime.funcval)(0x4c97f8),
_panic: *runtime._panic nil,
link: *(*runtime._defer)(0xc00008eed0),},}
前面都是遇到defer就将其加到gp._defer链表中,deferreturn才是真正执行的时候.
这里gp._defer = d.link
相当于取出了最后一个defer, 然后调用jmpdefer执行串成了一个链表,怎么区分多个函数的defer呢,这里就通过sp指针,判断caller中sp指针和defer当时的sp指针来判断.
在这里调用了freedefer(d),会将当前d放到pp.deferpool
中,类似于p.cache,是defer的本地缓存,当然如果本地缓存满了,会将pp.deferpool
的数据放一半到sched.deferpool
runtime.jmpdefer
中,b
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
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
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
在jmpdefer中,当执行完后,
// void jmpdefer(fv, sp);
// called from deferreturn.
// 1. grab stored LR for caller
// 2. sub 4 bytes to get back to BL deferreturn
// 3. BR to fn
(base) ➜ readsrc go tool objdump -s "runtime.jmpdefer" ./test
TEXT runtime.jmpdefer(SB) /usr/local/go/src/runtime/asm_amd64.s
asm_amd64.s:587 0x452dc0 488b542408 MOVQ 0x8(SP), DX// 第一个参数,fn地址
asm_amd64.s:588 0x452dc5 488b5c2410 MOVQ 0x10(SP), BX // 第二个参数arg0
asm_amd64.s:589 0x452dca 488d63f8 LEAQ -0x8(BX), SP //call deferreturn时压入的caller IP指针
asm_amd64.s:590 0x452dce 488b6c24f8 MOVQ -0x8(SP), BP // call的上一个地址,改为基址
asm_amd64.s:591 0x452dd3 48832c2405 SUBQ $0x5, 0(SP) //减去call指令,即下一次要执行call deferreturn
asm_amd64.s:592 0x452dd8 488b1a MOVQ 0(DX), BX // 压入fn函数
asm_amd64.s:593 0x452ddb ffe3 JMP BX 跳转到fn函数执行,用JMP而不是CALL,因为是同一个函数里面
如果中途调用goexit终止,他会负责处理整个调用堆栈的延迟函数
func Goexit() {
gp := getg()
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
d._panic = nil
}
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
d.started = true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
if gp._defer != d {
throw("bad defer entry in Goexit")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
// Note: we ignore recovers here because Goexit isn't a panic
}
}
性能
延迟调用远不是一个call指令那么简单,会涉及到对象分配,缓存和多次函数调用。 在性能要求比较高的场合,应该避免使用defer,go1.13测试的时候有3x的性能差距
BenchmarkNormal-12 100000000 11.2 ns/op
BenchmarkDefer-12 37844540 31.1 ns/op
panic
panic和defer的实现类似,也是放在gp._panic上面
如果recovered,那么会调用recover,recover会调用gogo(&gp.sched)
,否则defer结束后打印panic
func gopanic(e interface{}) {
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
d := gp._defer
if d == nil {
break
}
if d.started { //如果已经开始了,则执行下一个
continue
}
if p.recovered { //如果defer中执行了recovered,
mcall(recovery) //调用recover继续执行
throw("recovery failed") // mcall should not return
}
preprintpanics(gp._panic)
fatalpanic(gp._panic) // should not return
}
recover的实现是gorecover
调用后,判断gp._panic
如果不为nil, 且不是recovered状态,那么设置其p.recovered=true,改为已恢复状态
注意: 这里也通过p.argp指针和当前的调用指针比较来区分不同函数的panic。
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
有疑问加站长微信联系(非本文作者)