GO 中 defer的實現原理
GO 中 defer的實現原理
我們來回顧一下上次的分享,分享了關於 通道的一些知識點
- 分享了 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.go
的 type _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中宣告2個defer
,先宣告 defer test1()
再宣告 defer test2()
可以看出後宣告的defer
會插入到單鏈表的頭,先宣告的defer
被排到後面去了
咱們取的時候也是一直取頭下來執行,直到單鏈表為空。
咱一起來看看defer
的具體實現
原始碼檔案在 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
重新分配,且設定好 對應的成員 siz
和 heap
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玩一下驗證碼
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~