In-depth understanding of Go defer

  Back end, golang, php

In the previous chapterIn-depth understanding of Go panic and recoverWe found outdeferWith its great relevance, I still think it is very necessary to go deeper. I hope that through this chapter you can be rightdeferKeyword has a profound understanding, so let’s begin. You wait first, please line up. We use LIFO here …

image

Original address:In-depth understanding of Go defer

Characteristics

Let’s have a quick lookdeferThe basic use of keywords allows everyone to have a basic understanding first.

I. Delayed Call

func main() {
    defer log.Println("EDDYCJY.")

    log.Println("end.")
}

Output results:

$ go run main.go            
2019/05/19 21:15:02 end.
2019/05/19 21:15:02 EDDYCJY.

Second, LIFO

func main() {
    for i := 0; i < 6; i++ {
        defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".")
    }


    log.Println("end.")
}

Output results:

$ go run main.go
2019/05/19 21:19:17 end.
2019/05/19 21:19:17 EDDYCJY5.
2019/05/19 21:19:17 EDDYCJY4.
2019/05/19 21:19:17 EDDYCJY3.
2019/05/19 21:19:17 EDDYCJY2.
2019/05/19 21:19:17 EDDYCJY1.
2019/05/19 21:19:17 EDDYCJY0.

III. Operating Time Points

func main() {
    func() {
         defer log.Println("defer.EDDYCJY.")
    }()

    log.Println("main.EDDYCJY.")
}

Output results:

$ go run main.go 
2019/05/22 23:30:27 defer.EDDYCJY.
2019/05/22 23:30:27 main.EDDYCJY.

IV. Exception Handling

func main() {
    defer func() {
        if e := recover(); e != nil {
            log.Println("EDDYCJY.")
        }
    }()

    panic("end.")
}

Output results:

$ go run main.go 
2019/05/20 22:22:57 EDDYCJY.

Source code analysis

$ go tool compile -S main.go 
"".main STEXT size=163 args=0x0 locals=0x40
    ...
    0x0059 00089 (main.go:6)    MOVQ    AX, 16(SP)
    0x005e 00094 (main.go:6)    MOVQ    $1, 24(SP)
    0x0067 00103 (main.go:6)    MOVQ    $1, 32(SP)
    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)
    0x0075 00117 (main.go:6)    TESTL    AX, AX
    0x0077 00119 (main.go:6)    JNE    137
    0x0079 00121 (main.go:7)    XCHGL    AX, AX
    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP
    0x0084 00132 (main.go:7)    ADDQ    $64, SP
    0x0088 00136 (main.go:7)    RET
    0x0089 00137 (main.go:6)    XCHGL    AX, AX
    0x008a 00138 (main.go:6)    CALL    runtime.deferreturn(SB)
    0x008f 00143 (main.go:6)    MOVQ    56(SP), BP
    0x0094 00148 (main.go:6)    ADDQ    $64, SP
    0x0098 00152 (main.go:6)    RET
    ...

First we need to find it and find out what execution code it actually corresponds to. By compiling the code, we can know that the following methods are involved:

  • runtime.deferproc
  • runtime.deferreturn

Obviously, it is the runtime method and the right person. Let’s continue to go down and see what actions have been taken respectively.

Data structure

Before we begin, we need to introducedeferThe basic unit of_deferStructure, as follows:

type _defer struct {
    siz     int32
    started bool
    sp      uintptr // sp at time of defer
    pc      uintptr
    fn      *funcval
    _panic  *_panic // panic that is running defer
    link    *_defer
}

...
type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}
  • Siz: Total Size of All Incoming Parameters
  • Started: It’s timedeferHas it been implemented
  • Sp: Function Stack Pointer Register, which generally points to the top of the current function stack
  • Pc: Program counter, sometimes called instruction pointer (IP), which threads use to track the next instruction to be executed. In most processors, the PC points to the next instruction, not the current instruction
  • Fn: Point to the function address and parameters passed in
  • _panic: pointing to_panicLinked list
  • Link: point to_deferLinked list

image

deferproc

func deferproc(siz int32, fn *funcval) {
    ...
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()

    d := newdefer(siz)
    ...
    d.fn = fn
    d.pc = callerpc
    d.sp = sp
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }

    return0()
}
  • Get calldeferThe function stack pointer of the function, the specific address of the parameter of the incoming function, and PC (program counter), which is the next instruction to be executed. These are equivalent to preparatory parameters for subsequent flow control.
  • Create a newdeferMinimum cell_deferTo fill in the previously prepared parameters
  • callmemmoveStores the passed-in parameters to the new_defer(currently in use) to facilitate subsequent use
  • Last callreturn0To return, this function is very important. Can be avoided indeferprocBecause of the returnreturn, and induceddeferreturnThe call to the method. The root cause is a haltpanicThe delay method of enables thedeferprocReturns 1, but in the mechanism ifdeferprocIf the return value is not equal to 0, it will always check the return value and jump to the end of the function. Andreturn0The return is 0, so repeated calls can be prevented.

Summary

InThis function will be new_deferSet some basic properties and pass in the parameter set of the calling function. Finally, the function call is ended by a special return method. In addition, this one is similar to the previous one.In-depth understanding of Go panic and recoverThe processing logic of has certain relevance, in fact isgp.sched.retReturns 0 or 1 will be diverted to different processing methods

newdefer

func newdefer(siz int32) *_defer {
    var d *_defer
    sc := deferclass(uintptr(siz))
    gp := getg()
    if sc < uintptr(len(p{}.deferpool)) {
        pp := gp.m.p.ptr()
        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
            ...
            lock(&sched.deferlock)
            d := sched.deferpool[sc]
            unlock(&sched.deferlock)
        }
        ...
    }
    if d == nil {
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
        ...
    }
    d.siz = siz
    d.link = gp._defer
    gp._defer = d
    return d
}
  • Get available from the pool_defer, reuse as a new base unit
  • If there is no available in the pool, callmallocgcRe-apply for a new one
  • Set updeferFinally, modify the currentGoroutineThe_deferpoint to

Through this method, we can notice two points, as follows:

  • deferAndGoroutine(g)There is a direct relationship, so the discussiondeferWhen basic cannot leavegThe association of
  • NewdeferIt will always be at the front of the existing linked list, that isdeferLast in, first out

Summary

This function is mainly responsible for obtaining the new_deferThe role of it may be fromdeferpoolIt is also possible to apply again

deferreturn

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)))
}

If called in a methoddeferKeyword, the compiler will insert at the enddeferreturnThe call to the method. This method mainly includes the following steps:

  • Clear current node_deferCalled function call information
  • The that releases the current node_defer, and put it back into the pool (easy to reuse)
  • Jump to calldeferAt the calling function of the keyword

In this code, the jump methodjmpdeferEspecially important. Because it explicitly controls the flow, the code is as follows:

// asm_amd64.s
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
    MOVQ    fv+0(FP), DX    // fn
    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

Through the analysis of the source code, we found that it did two very “strange” and important things, as follows:

  • MOVQ -8(SP), BP:-8(BX)This location holdsdeferreturnAddress after execution
  • SUBQ $5, (SP):SPIf you subtract 5 from your address, the length you subtract is exactlyruntime.deferreturnThe length of

You may ask, why is it 5? All right. After half a day of searching, I finally looked at the assembly code … well, subtraction is definitely 5. there is nothing wrong with it, as follows:

    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP

Let’s sort out our thoughts, according to the above logic, thendeferreturnIs a “recursive” oh. Every time I go backdeferreturnFunction, then when will it end, as follows:

func deferreturn(arg0 uintptr) {
    gp := getg()
    d := gp._defer
    if d == nil {
        return
    }
    ...
}

That is, will continue to enterdeferreturnFunction to determine whether there is still a list_defer. If it no longer exists, return and end it. To put it simply, it is to deal with the whole department.deferTo allow you to really leave it. Is this really the case? Let’s look at the assembly code above, as follows:

    。..
    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)
    0x0075 00117 (main.go:6)    TESTL    AX, AX
    0x0077 00119 (main.go:6)    JNE    137
    0x0079 00121 (main.go:7)    XCHGL    AX, AX
    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP
    0x0084 00132 (main.go:7)    ADDQ    $64, SP
    0x0088 00136 (main.go:7)    RET
    0x0089 00137 (main.go:6)    XCHGL    AX, AX
    0x008a 00138 (main.go:6)    CALL    runtime.deferreturn(SB)
    ...

It is true that the analysis is consistent with the above process, and the verification is completed.

Summary

This function is mainly responsible for emptying the useddeferAnd jump to calldeferKeyword function, is very important

Summary

We mentioned thatdeferKeyword involves two core functions, namelydeferprocAnddeferreturnFunction. AnddeferreturnFunction is special, when application function callsdeferKeyword, the compiler inserts at the end of itdeferreturnThe call of the, they are usually in pairs

But when oneGoroutineThere are many times in the worlddeferBehavior (i.e. multiple_defer), the compiler will make use of some small tricks to go back todeferreturnFunction to consume_deferThe linked list is not allowed to end until there is none left.

The new basic unit_defer, may be reused, may also be a new application. It will eventually be appended to the_deferThe header of the linked list, thus setting the last-in-first-out calling feature.

correlation

References