深刻理解 Go defer

在上一章節 《深刻理解 Go panic and recover》 中,咱們發現了 defer 與其關聯性極大,仍是以爲很是有必要深刻一下。但願經過本章節你們能夠對 defer 關鍵字有一個深入的理解,那麼咱們開始吧。你先等等,請排好隊,咱們這兒採起後進先出 LIFO 的出站方式...html

image

原文地址:深刻理解 Go defergit

特性

咱們簡單的過一下 defer 關鍵字的基礎使用,讓你們先有一個基礎的認知github

1、延遲調用

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

    log.Println("end.")
}

輸出結果:golang

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

2、後進先出

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


    log.Println("end.")
}

輸出結果:segmentfault

$ 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.

3、運行時間點

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

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

輸出結果:數據結構

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

4、異常處理

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

    panic("end.")
}

輸出結果:less

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

源碼剖析

$ 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
    ...

首先咱們須要找到它,找到它實際對應什麼執行代碼。經過彙編代碼,可得知涉及以下方法:函數

  • runtime.deferproc
  • runtime.deferreturn

很顯然是運行時的方法,是對的人。咱們繼續往下走看看都分別承擔了什麼行爲ui

數據結構

在開始前咱們須要先介紹一下 defer 的基礎單元 _defer 結構體,以下:spa

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:全部傳入參數的總大小
  • started:該 defer 是否已經執行過
  • sp:函數棧指針寄存器,通常指向當前函數棧的棧頂
  • pc:程序計數器,有時稱爲指令指針(IP),線程利用它來跟蹤下一個要執行的指令。在大多數處理器中,PC指向的是下一條指令,而不是當前指令
  • fn:指向傳入的函數地址和參數
  • _panic:指向 _panic 鏈表
  • link:指向 _defer 鏈表

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()
}
  • 獲取調用 defer 函數的函數棧指針、傳入函數的參數具體地址以及PC (程序計數器),也就是下一個要執行的指令。這些至關因而預備參數,便於後續的流轉控制
  • 建立一個新的 defer 最小單元 _defer,填入先前準備的參數
  • 調用 memmove 將傳入的參數存儲到新 _defer (當前使用)中去,便於後續的使用
  • 最後調用 return0 進行返回,這個函數很是重要。可以避免在 deferproc 中又由於返回 return,而誘發 deferreturn 方法的調用。其根本緣由是一箇中止 panic 的延遲方法會使 deferproc 返回 1,但在機制中若是 deferproc 返回不等於 0,將會老是檢查返回值並跳轉到函數的末尾。而 return0 返回的就是 0,所以能夠防止重複調用

小結

這個函數中會爲新的 _defer 設置一些基礎屬性,並將調用函數的參數集傳入。最後經過特殊的返回方法結束函數調用。另外這一塊與先前 《深刻理解 Go panic and recover》 的處理邏輯有必定關聯性,其實就是 gp.sched.ret 返回 0 仍是 1 會分流至不一樣處理方式

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
}
  • 從池中獲取可使用的 _defer,則複用做爲新的基礎單元
  • 若在池中沒有獲取到可用的,則調用 mallocgc 從新申請一個新的
  • 設置 defer 的基礎屬性,最後修改當前 Goroutine_defer 指向

經過這個方法咱們能夠注意到兩點,以下:

  • deferGoroutine(g) 有直接關係,因此討論 defer 時基本離不開 g 的關聯
  • 新的 defer 老是會在現有的鏈表中的最前面,也就是 defer 的特性後進先出

小結

這個函數主要承擔了獲取新的 _defer 的做用,它有多是從 deferpool 中獲取的,也有多是從新申請的

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

若是在一個方法中調用過 defer 關鍵字,那麼編譯器將會在結尾處插入 deferreturn 方法的調用。而該方法中主要作了以下事項:

  • 清空當前節點 _defer 被調用的函數調用信息
  • 釋放當前節點的 _defer 的存儲信息並放回池中(便於複用)
  • 跳轉到調用 defer 關鍵字的調用函數處

在這段代碼中,跳轉方法 jmpdefer 格外重要。由於它顯式的控制了流轉,代碼以下:

// 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

經過源碼的分析,咱們發現它作了兩個很 「奇怪」 又很重要的事,以下:

  • MOVQ -8(SP), BP:-8(BX) 這個位置保存的是 deferreturn 執行完畢後的地址
  • SUBQ $5, (SP):SP 的地址減 5 ,其減掉的長度就剛好是 runtime.deferreturn 的長度

你可能會問,爲何是 5?好吧。翻了半天最後看了一下彙編代碼...嗯,相減的確是 5 沒毛病,以下:

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

咱們整理一下思緒,照上述邏輯的話,那 deferreturn 就是一個 「遞歸」 了哦。每次都會從新回到 deferreturn 函數,那它在何時纔會結束呢,以下:

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

也就是會不斷地進入 deferreturn 函數,判斷鏈表中是否還存着 _defer。若已經不存在了,則返回,結束掉它。簡單來說,就是處理徹底部 defer 才容許你真的離開它。果然如此嗎?咱們再看看上面的彙編代碼,以下:

。..
    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)
    ...

的確如上述流程所分析一致,驗證完畢

小結

這個函數主要承擔了清空已使用的 defer 和跳轉到調用 defer 關鍵字的函數處,很是重要

總結

咱們有提到 defer 關鍵字涉及兩個核心的函數,分別是 deferprocdeferreturn 函數。而 deferreturn 函數比較特殊,是當應用函數調用 defer 關鍵字時,編譯器會在其結尾處插入 deferreturn 的調用,它們倆通常都是成對出現的

可是當一個 Goroutine 上存在着屢次 defer 行爲(也就是多個 _defer)時,編譯器會進行利用一些小技巧, 從新回到 deferreturn 函數去消耗 _defer 鏈表,直到一個不剩才容許真正的結束

而新增的基礎單元 _defer,有多是被複用的,也有多是全新申請的。它最後都會被追加到 _defer 鏈表的表頭,從而設定了後進先出的調用特性

關聯

參考

相關文章
相關標籤/搜索