GO 中 defer的實現原理

小魔童哪吒發表於2021-06-20
[TOC]

我們來回顧一下上次的分享,分享了關於 通道的一些知識點

  • 分享了 GO 中通道是什麼
  • 通道的底層資料結構詳細解析
  • 通道在GO原始碼中是如何實現的
  • Chan 讀寫的基本原理
  • 關閉通道會出現哪些異常,panic
  • select 的簡單應用

要是對 chan 通道還有點興趣的話,歡迎檢視文章 GO 中 Chan 實現原理分享

defer 是什麼?

我們們一起來看看 defer 是個啥

是 GO 中的一個關鍵字

這個關鍵字,我們一般用在釋放資源,在 return 前會呼叫他

如果程式中有多個 defer ,defer 的呼叫順序是按照類似的方式,後進先出 LIFO的 ,這裡順便寫一下

遵循後進先出原則

後進入棧的,先出棧

先進入棧的,後出棧

  • 佇列

遵循先進先出 , 我們就可以想象一個單向的管道,從左邊進,右邊出

先進來,先出去

後進來,後出去,不準插隊

defer 實現原理

我們們先丟擲一個結論,先心裡有點底:

  • 程式碼中宣告 defer的位置,編譯的時候會插入一個函式叫做 deferproc ,在該defer所在的函式前插入一個返回的函式,不是return 哦,是deferreturn

具體的 defer 的實現原理是咋樣的,我們還是一樣的,來看看 defer的底層資料結構是啥樣的 ,

src/runtime/runtime2.gotype _defer struct {結構

// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer and deferProcStack
// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
// and cmd/compile/internal/gc/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
   siz     int32 // includes both arguments and results
   started bool
   heap    bool
   // openDefer indicates that this _defer is for a frame with open-coded
   // defers. We have only one defer record for the entire frame (which may
   // currently have 0, 1, or more defers active).
   openDefer bool
   sp        uintptr  // sp at time of defer
   pc        uintptr  // pc at time of defer
   fn        *funcval // can be nil for open-coded defers
   _panic    *_panic  // panic that is running defer
   link      *_defer

   // If openDefer is true, the fields below record values about the stack
   // frame and associated function that has the open-coded defer(s). sp
   // above will be the sp for the frame, and pc will be address of the
   // deferreturn call in the function.
   fd   unsafe.Pointer // funcdata for the function associated with the frame
   varp uintptr        // value of varp for the stack frame
   // framepc is the current pc associated with the stack frame. Together,
   // with sp above (which is the sp associated with the stack frame),
   // framepc/sp can be used as pc/sp pair to continue a stack trace via
   // gentraceback().
   framepc uintptr
}

_defer 持有延遲呼叫列表中的一個條目 ,我們來看看上述資料結構的引數都是啥意思

tag 說明
siz defer函式的引數和結果的記憶體大小
fn 需要被延遲執行的函式
_panic defer 的 panic 結構體
link 同一個協程裡面的defer 延遲函式,會通過該指標連線在一起
heap 是否分配在堆上面
openDefer 是否經過開放編碼優化
sp 棧指標(一般會對應到彙編)
pc 程式計數器

defer 關鍵字後面必須是跟函式,這一點我們們要記住哦

通過上述引數的描述,我們可以知道,defer的資料結構和函式類似,也是有如下三個引數:

  • 棧指標 SP
  • 程式計數器 PC
  • 函式的地址

可是我們是不是也發現了,成員裡面還有一個link,同一個協程裡面的defer 延遲函式,會通過該指標連線在一起

這個link指標,是指向的一個defer單連結串列的頭,每次我們們宣告一個defer的時候,就會將該defer的資料插入到這個單連結串列頭部的位置,

那麼,執行defer的時候,我們是不是就能猜到defer 是咋取得了不?

前面有說到defer是後進先出的,這裡當然也是遵循這個道理,取defer進行執行的時候,是從單連結串列的頭開始去取的。

我們們來畫個圖形象一點

在協程A中宣告2defer,先宣告 defer test1()

再宣告 defer test2()

可以看出後宣告的defer會插入到單連結串列的頭,先宣告的defer被排到後面去了

我們們取的時候也是一直取頭下來執行,直到單連結串列為空。

我們一起來看看defer 的具體實現

image-20210618144713620

原始碼檔案在 src/runtime/panic.go 中,檢視 函式 deferproc

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
   gp := getg()
   if gp.m.curg != gp {
      // go code on the system stack can't defer
      throw("defer on system stack")
   }

   // the arguments of fn are in a perilous state. The stack map
   // for deferproc does not describe them. So we can't let garbage
   // collection or stack copying trigger until we've copied them out
   // to somewhere safe. The memmove below does that.
   // Until the copy completes, we can only call nosplit routines.
   sp := getcallersp()
   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
   callerpc := getcallerpc()

   d := newdefer(siz)
   if d._panic != nil {
      throw("deferproc: d.panic != nil after newdefer")
   }
   d.link = gp._defer
   gp._defer = d
   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))
   }

   // deferproc returns 0 normally.
   // a deferred func that stops a panic
   // makes the deferproc return 1.
   // the code the compiler generates always
   // checks the return value and jumps to the
   // end of the function if deferproc returns != 0.
   return0()
   // No code can go here - the C return register has
   // been set and must not be clobbered.
}

deferproc 的作用是:

建立一個新的遞延函式 fn,引數為 siz 位元組,編譯器將一個延遲語句轉換為對this的呼叫

getcallersp()

得到deferproc之前的rsp暫存器的值,實現的方式所有平臺都是一樣的

//go:noescape
func getcallersp() uintptr // implemented as an intrinsic on all platforms

callerpc := getcallerpc()

此處得到 rsp之後,儲存在 callerpc 中 , 此處是為了呼叫 deferproc 的下一條指令

d := newdefer(siz)

d := newdefer(siz) 新建一個defer 的結構,後續的程式碼是在給defer 這個結構的成員賦值

我們看看 deferproc 的大體流程:

  • 獲取 deferproc之前的rsp暫存器的值
  • 使用newdefer 分配一個 _defer 結構體物件,並且將他放到當前的 _defer 連結串列的頭
  • 初始化_defer 的相關成員引數
  • return0

來我們看看 newdefer的原始碼

原始碼檔案在 src/runtime/panic.go 中,檢視函式newdefer


// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer.  The defer is not
// added to any defer chain yet.
//
// This must not grow the stack because there may be a frame without
// stack map information when this is called.
//
//go:nosplit
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 {
            // Take the slow path on the system stack so
            // we don't grow newdefer's stack.
            systemstack(func() {
                lock(&sched.deferlock)
                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }
                unlock(&sched.deferlock)
            })
        }
        if n := len(pp.deferpool[sc]); n > 0 {
            d = pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
        }
    }
    if d == nil {
        // Allocate new defer+args.
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
    }
    d.siz = siz
    d.heap = true
    return d
}

newderfer 的作用:

通常使用per-P池,分配一個Defer

每個defer可以自由的釋放。當前defer 也不會加入任何一個 defer鏈條中

getg()

獲取當前協程的結構體指標

// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g

pp := gp.m.p.ptr()

拿到當前工作執行緒裡面的 P

然後拿到 從全域性的物件池子中拿一部分物件給到P的池子裡面

for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }

點進去看池子的資料結構,其實裡面的成員也就是 我們們之前說到的 _defer指標

其中 sched.deferpool[sc] 是全域性的池子,pp.deferpool[sc] 是本地的池子

mallocgc分配空間

上述操作若 d 沒有拿到值,那麼就直接使用 mallocgc 重新分配,且設定好 對應的成員 sizheap

if d == nil {
        // Allocate new defer+args.
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
    }
d.siz = siz
d.heap = true

mallocgc 具體實現在 src/runtime/malloc.go 中,若感興趣的話,可以深入看看這一塊,今天我們們不重點說這個函式

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}

最後再來看看return0

最後再來看看 deferproc 函式中的 結果返回return0()

// return0 is a stub used to return 0 from deferproc.
// It is called at the very end of deferproc to signal
// the calling Go function that it should not jump
// to deferreturn.
// in asm_*.s
func return0()

return0 是用於從deferproc返回0的存根

它在deferproc函式的最後被呼叫,用來通知呼叫Go的函式它不應該跳轉到deferreturn

在正常情況下 return0 正常返回 0

可是異常情況下 return0 函式會返回 1,此時GO 就會跳轉到執行 deferreturn

簡單說下 deferreturn

deferreturn的作用就是情況defer裡面的連結串列,歸還相應的緩衝區,或者把對應的空間讓GC回收調

GO 中 defer 的規則

上面分析了GO 中defer 的實現原理之後,我們們現在來了解一下 GO 中應用defer 是需要遵守 3 個規則的,我們們來列一下:

  • defer後面跟的函式,叫延遲函式,函式中的引數在defer語句宣告的時候,就已經確定下來了
  • 延遲函式的執行時按照後進先出來的,文章前面也多次說到過,這個印象應該很深刻吧,先出現的defer後執行,後出現的defer先執行
  • 延遲函式可能會影響到整個函式的返回值

我們們還是要來解釋一下的,上面第 2 點,應該都好理解,上面的圖也表明了 執行順序

第一點我們們來寫個小DEMO

延遲函式中的引數在defer語句宣告的時候,就已經確定下來了

func main() {
   num := 1
   defer fmt.Println(num)

   num++

   return
}

別猜了,執行結果是 1,小夥伴們可以將程式碼拷貝下來,自己執行一波

第三點也來一個DEMO

延遲函式可能會影響到整個函式的返回值

func test3() (res int) {
   defer func() {
      res++
   }()

   return 1
}
func main() {

   fmt.Println(test3())

   return
}

上述程式碼,我們在 test3函式中的返回值,我們提前命名好了,本來應該是返回結果為 1

可是在return 這裡,執行順序這樣的

res = 1

res++

因此,結果就是 2

總結

  • 分享了defer是什麼
  • 簡單示意了棧和佇列
  • defer的資料結構和實現原理,具體的原始碼展示
  • GO中defer的 3 條規則

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,下一次 我們用GO玩一下驗證碼

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章