美文网首页
golang 源码剖析(7): 延迟defer

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

作者: darcyaf | 来源:发表于2020-03-08 17:29 被阅读0次

简介

延迟调用(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 $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
}

相关文章

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

    简介 延迟调用(defer)的优势是: 即使函数执行出错,依然能保证回收资源等操作得以执行 可以在变量的定义处加入...

  • 12 Golang defer panic recover

    defer Golang中的defer会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处...

  • golang语言defer特性详解.md

    [TOC] golang语言defer特性详解 defer语句是go语言提供的一种用于注册延迟调用的机制,它可以让...

  • Golang之Defer

    引用 golang defer实现原理 Golang之轻松化解defer的温柔陷阱 Golang中defer、re...

  • Golang defer总结

    0 Golang有一个特殊的控件语句,那就是defer,defer语句用于延迟调用指定的函数,比如释放资源等,它会...

  • Golang-defer关键字

    defer关键字 defer是Golang中一个非常重要的关键字,主要是用于注册延迟调用,这些调用在return时...

  • golang延迟调用函数defer

    defer语句被⽤于预定对⼀个函数的调⽤。可以把这类被defer语句调⽤的函数称为延迟函数。 延迟的函数是按照后进...

  • 2017-12-04

    Golang,Panic,Defer,Recover 在golang中,recover在defer里发挥作用。 一...

  • go defer理解

    golang的defer是怎么工作的?defer在golang里是一个很基础的关键字,在函数内部使用defer声明...

  • Golang(十五)defer语句

    defer` 1.1 延迟是什么? 即延迟(defer)语句,延迟语句被用于执行一个函数调用,在这个函数之前,延迟...

网友评论

      本文标题:golang 源码剖析(7): 延迟defer

      本文链接:https://www.haomeiwen.com/subject/rmpddhtx.html