摘要:在上一章節深入理解中,我們發現了與其關聯性極大,還是覺得非常有必要深入一下。而返回的就是,因此可以防止重復調用小結在這個函數中會為新的設置一些基礎屬性,并將調用函數的參數集傳入。
在上一章節 《深入理解 Go panic and recover》 中,我們發現了 defer 與其關聯性極大,還是覺得非常有必要深入一下。希望通過本章節大家可以對 defer 關鍵字有一個深刻的理解,那么我們開始吧。你先等等,請排好隊,我們這兒采取后進先出 LIFO 的出站方式...
原文地址:深入理解 Go defer
特性我們簡單的過一下 defer 關鍵字的基礎使用,讓大家先有一個基礎的認知
一、延遲調用func main() { defer log.Println("EDDYCJY.") log.Println("end.") }
輸出結果:
$ go run main.go 2019/05/19 21:15:02 end. 2019/05/19 21:15:02 EDDYCJY.二、后進先出
func main() { for i := 0; i < 6; i++ { defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".") } log.Println("end.") }
輸出結果:
$ 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.三、運行時間點
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.四、異常處理
func main() { defer func() { if e := recover(); e != nil { log.Println("EDDYCJY.") } }() panic("end.") }
輸出結果:
$ 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
很顯然是運行時的方法,是對的人。我們繼續往下走看看都分別承擔了什么行為
數據結構在開始前我們需要先介紹一下 defer 的基礎單元 _defer 結構體,如下:
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 鏈表
deferprocfunc 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 會分流至不同處理方式
newdeferfunc 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 指向
通過這個方法我們可以注意到兩點,如下:
defer 與 Goroutine(g) 有直接關系,所以討論 defer 時基本離不開 g 的關聯
新的 defer 總是會在現有的鏈表中的最前面,也就是 defer 的特性后進先出
小結這個函數主要承擔了獲取新的 _defer 的作用,它有可能是從 deferpool 中獲取的,也有可能是重新申請的
deferreturnfunc 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 關鍵字涉及兩個核心的函數,分別是 deferproc 和 deferreturn 函數。而 deferreturn 函數比較特殊,是當應用函數調用 defer 關鍵字時,編譯器會在其結尾處插入 deferreturn 的調用,它們倆一般都是成對出現的
但是當一個 Goroutine 上存在著多次 defer 行為(也就是多個 _defer)時,編譯器會進行利用一些小技巧, 重新回到 deferreturn 函數去消耗 _defer 鏈表,直到一個不剩才允許真正的結束
而新增的基礎單元 _defer,有可能是被復用的,也有可能是全新申請的。它最后都會被追加到 _defer 鏈表的表頭,從而設定了后進先出的調用特性
關聯深入理解 Go panic and recover
參考Scheduling In Go
Dive into stack and defer/panic/recover in go
golang-notes
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31599.html
摘要:恢復流程如下判斷當前中的是否已標注為處理從鏈表中刪除已標注中止的事件,也就是刪除已經被恢復的事件將相關需要恢復的棧幀信息傳遞給方法的參數每個棧幀對應著一個未運行完的函數。 作為一個 gophper,我相信你對于 panic 和 recover 肯定不陌生,但是你有沒有想過。當我們執行了這兩條語句之后。底層到底發生了什么事呢?前幾天和同事剛好聊到相關的話題,發現其實大家對這塊理解還是比較...
摘要:原文地址會有性能損耗,盡量不要用上個月在軒脈刃的全棧技術群里看到一個小伙伴問說在棧退出時執行,會有性能損耗,盡量不要用,這個怎么解。因此,對于會有性能損耗,盡量不能用這個問題,我認為該用就用,應該及時關閉就不要延遲,在用時一定要想清楚場景。 showImg(https://i.imgur.com/YlKjnSH.jpg); 原文地址:Go defer 會有性能損耗,盡量不要用? 上個月...
摘要:為語言提供了強大的協程編程模式。提供的協程語法借鑒自,在此向開發組致敬協程可以與很好地互補。并發執行使用創建協程,可以讓和兩個函數變成并發執行。協程需要拿到請求的結果。 Swoole4為PHP語言提供了強大的CSP協程編程模式。底層提供了3個關鍵詞,可以方便地實現各類功能。 Swoole4提供的PHP協程語法借鑒自Golang,在此向GO開發組致敬 PHP+Swoole協程可以與...
摘要:還有一種情況就是當你在一行中寫了多個語句,也需要使用分號來分開由于語言詞法分析器添加分號的特殊性,所以在有些情況下需要注意你都不應該將一個控制結構或的左大括號放在下一行。 Go語言中變量的聲明和JavaScript很像,使用var關鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個變量 var m int = 10 // 聲明初始化多個變量 var i, j...
閱讀 2123·2023-04-25 14:56
閱讀 2441·2021-11-16 11:44
閱讀 2696·2021-09-22 15:00
閱讀 1903·2019-08-29 16:55
閱讀 2177·2019-08-29 14:04
閱讀 2305·2019-08-29 11:23
閱讀 3678·2019-08-26 10:46
閱讀 1908·2019-08-22 18:43