分享
  1. 首页
  2. 文章

golang 源码剖析(7): 延迟defer

darcyaf · · 1029 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

简介

延迟调用(defer)的优势是:

  1. 即使函数执行出错,依然能保证回收资源等操作得以执行
  2. 可以在变量的定义处加入defer,代码结构上避免忘记做某些数据的回收

劣势:

  1. 性能上会会比直接调用慢一些
  2. 如果在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 0ドルxa8, 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 0ドルx30, 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 0ドルx1, 0x68(SP)
 main.go:8 0x48cfb2 48c744247001000000 MOVQ 0ドルx1, 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 0ドルxa8, 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 0ドルxa8, 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 0ドルx5, 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
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:darcyaf

查看原文:golang 源码剖析(7): 延迟defer

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1029 次点击 ∙ 1 赞
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏