網站首頁 編程語言 正文
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 條規則
原文鏈接:https://juejin.cn/post/6975686540601245709
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2023-10-11 MP、MybatisPlus、聯表查詢、自定義sql、Constants.WRAPPER、ew (一
- 2022-10-24 Winform控件優化之圓角按鈕2_C#教程
- 2022-04-14 教你用python將數據寫入Excel文件中_python
- 2022-05-20 C++實現簡單學生信息管理系統_C 語言
- 2023-06-18 C#?System.TypeInitializationException?異常處理方案_C#教程
- 2024-02-25 Navicat提示Access violation at address **** in modul
- 2023-08-13 uniapp寫一個隨時間變化的預約日期列表
- 2022-09-04 使用Django+Pytest搭建在線自動化測試平臺_python
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支