摘要:在上一章節(jié)深入理解中,我們發(fā)現(xiàn)了與其關(guān)聯(lián)性極大,還是覺(jué)得非常有必要深入一下。而返回的就是,因此可以防止重復(fù)調(diào)用小結(jié)在這個(gè)函數(shù)中會(huì)為新的設(shè)置一些基礎(chǔ)屬性,并將調(diào)用函數(shù)的參數(shù)集傳入。
在上一章節(jié) 《深入理解 Go panic and recover》 中,我們發(fā)現(xiàn)了 defer 與其關(guān)聯(lián)性極大,還是覺(jué)得非常有必要深入一下。希望通過(guò)本章節(jié)大家可以對(duì) defer 關(guān)鍵字有一個(gè)深刻的理解,那么我們開始吧。你先等等,請(qǐng)排好隊(duì),我們這兒采取后進(jìn)先出 LIFO 的出站方式...
原文地址:深入理解 Go defer
特性我們簡(jiǎn)單的過(guò)一下 defer 關(guān)鍵字的基礎(chǔ)使用,讓大家先有一個(gè)基礎(chǔ)的認(rèn)知
一、延遲調(diào)用func main() { defer log.Println("EDDYCJY.") log.Println("end.") }
輸出結(jié)果:
$ go run main.go 2019/05/19 21:15:02 end. 2019/05/19 21:15:02 EDDYCJY.二、后進(jìn)先出
func main() { for i := 0; i < 6; i++ { defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".") } log.Println("end.") }
輸出結(jié)果:
$ 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.三、運(yùn)行時(shí)間點(diǎn)
func main() { func() { defer log.Println("defer.EDDYCJY.") }() log.Println("main.EDDYCJY.") }
輸出結(jié)果:
$ 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.") }
輸出結(jié)果:
$ 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 ...
首先我們需要找到它,找到它實(shí)際對(duì)應(yīng)什么執(zhí)行代碼。通過(guò)匯編代碼,可得知涉及如下方法:
runtime.deferproc
runtime.deferreturn
很顯然是運(yùn)行時(shí)的方法,是對(duì)的人。我們繼續(xù)往下走看看都分別承擔(dān)了什么行為
數(shù)據(jù)結(jié)構(gòu)在開始前我們需要先介紹一下 defer 的基礎(chǔ)單元 _defer 結(jié)構(gòu)體,如下:
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:所有傳入?yún)?shù)的總大小
started:該 defer 是否已經(jīng)執(zhí)行過(guò)
sp:函數(shù)棧指針寄存器,一般指向當(dāng)前函數(shù)棧的棧頂
pc:程序計(jì)數(shù)器,有時(shí)稱為指令指針(IP),線程利用它來(lái)跟蹤下一個(gè)要執(zhí)行的指令。在大多數(shù)處理器中,PC指向的是下一條指令,而不是當(dāng)前指令
fn:指向傳入的函數(shù)地址和參數(shù)
_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() }
獲取調(diào)用 defer 函數(shù)的函數(shù)棧指針、傳入函數(shù)的參數(shù)具體地址以及PC (程序計(jì)數(shù)器),也就是下一個(gè)要執(zhí)行的指令。這些相當(dāng)于是預(yù)備參數(shù),便于后續(xù)的流轉(zhuǎn)控制
創(chuàng)建一個(gè)新的 defer 最小單元 _defer,填入先前準(zhǔn)備的參數(shù)
調(diào)用 memmove 將傳入的參數(shù)存儲(chǔ)到新 _defer (當(dāng)前使用)中去,便于后續(xù)的使用
最后調(diào)用 return0 進(jìn)行返回,這個(gè)函數(shù)非常重要。能夠避免在 deferproc 中又因?yàn)榉祷?return,而誘發(fā) deferreturn 方法的調(diào)用。其根本原因是一個(gè)停止 panic 的延遲方法會(huì)使 deferproc 返回 1,但在機(jī)制中如果 deferproc 返回不等于 0,將會(huì)總是檢查返回值并跳轉(zhuǎn)到函數(shù)的末尾。而 return0 返回的就是 0,因此可以防止重復(fù)調(diào)用
小結(jié)在這個(gè)函數(shù)中會(huì)為新的 _defer 設(shè)置一些基礎(chǔ)屬性,并將調(diào)用函數(shù)的參數(shù)集傳入。最后通過(guò)特殊的返回方法結(jié)束函數(shù)調(diào)用。另外這一塊與先前 《深入理解 Go panic and recover》 的處理邏輯有一定關(guān)聯(lián)性,其實(shí)就是 gp.sched.ret 返回 0 還是 1 會(huì)分流至不同處理方式
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,則復(fù)用作為新的基礎(chǔ)單元
若在池中沒(méi)有獲取到可用的,則調(diào)用 mallocgc 重新申請(qǐng)一個(gè)新的
設(shè)置 defer 的基礎(chǔ)屬性,最后修改當(dāng)前 Goroutine 的 _defer 指向
通過(guò)這個(gè)方法我們可以注意到兩點(diǎn),如下:
defer 與 Goroutine(g) 有直接關(guān)系,所以討論 defer 時(shí)基本離不開 g 的關(guān)聯(lián)
新的 defer 總是會(huì)在現(xiàn)有的鏈表中的最前面,也就是 defer 的特性后進(jìn)先出
小結(jié)這個(gè)函數(shù)主要承擔(dān)了獲取新的 _defer 的作用,它有可能是從 deferpool 中獲取的,也有可能是重新申請(qǐng)的
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))) }
如果在一個(gè)方法中調(diào)用過(guò) defer 關(guān)鍵字,那么編譯器將會(huì)在結(jié)尾處插入 deferreturn 方法的調(diào)用。而該方法中主要做了如下事項(xiàng):
清空當(dāng)前節(jié)點(diǎn) _defer 被調(diào)用的函數(shù)調(diào)用信息
釋放當(dāng)前節(jié)點(diǎn)的 _defer 的存儲(chǔ)信息并放回池中(便于復(fù)用)
跳轉(zhuǎn)到調(diào)用 defer 關(guān)鍵字的調(diào)用函數(shù)處
在這段代碼中,跳轉(zhuǎn)方法 jmpdefer 格外重要。因?yàn)樗@式的控制了流轉(zhuǎn),代碼如下:
// 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
通過(guò)源碼的分析,我們發(fā)現(xiàn)它做了兩個(gè)很 “奇怪” 又很重要的事,如下:
MOVQ -8(SP), BP:-8(BX) 這個(gè)位置保存的是 deferreturn 執(zhí)行完畢后的地址
SUBQ $5, (SP):SP 的地址減 5 ,其減掉的長(zhǎng)度就恰好是 runtime.deferreturn 的長(zhǎng)度
你可能會(huì)問(wèn),為什么是 5?好吧。翻了半天最后看了一下匯編代碼...嗯,相減的確是 5 沒(méi)毛病,如下:
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP
我們整理一下思緒,照上述邏輯的話,那 deferreturn 就是一個(gè) “遞歸” 了哦。每次都會(huì)重新回到 deferreturn 函數(shù),那它在什么時(shí)候才會(huì)結(jié)束呢,如下:
func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { return } ... }
也就是會(huì)不斷地進(jìn)入 deferreturn 函數(shù),判斷鏈表中是否還存著 _defer。若已經(jīng)不存在了,則返回,結(jié)束掉它。簡(jiǎn)單來(lái)講,就是處理完全部 defer 才允許你真的離開它。果真如此嗎?我們?cè)倏纯瓷厦娴膮R編代碼,如下:
。.. 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) ...
的確如上述流程所分析一致,驗(yàn)證完畢
小結(jié)這個(gè)函數(shù)主要承擔(dān)了清空已使用的 defer 和跳轉(zhuǎn)到調(diào)用 defer 關(guān)鍵字的函數(shù)處,非常重要
總結(jié)我們有提到 defer 關(guān)鍵字涉及兩個(gè)核心的函數(shù),分別是 deferproc 和 deferreturn 函數(shù)。而 deferreturn 函數(shù)比較特殊,是當(dāng)應(yīng)用函數(shù)調(diào)用 defer 關(guān)鍵字時(shí),編譯器會(huì)在其結(jié)尾處插入 deferreturn 的調(diào)用,它們倆一般都是成對(duì)出現(xiàn)的
但是當(dāng)一個(gè) Goroutine 上存在著多次 defer 行為(也就是多個(gè) _defer)時(shí),編譯器會(huì)進(jìn)行利用一些小技巧, 重新回到 deferreturn 函數(shù)去消耗 _defer 鏈表,直到一個(gè)不剩才允許真正的結(jié)束
而新增的基礎(chǔ)單元 _defer,有可能是被復(fù)用的,也有可能是全新申請(qǐng)的。它最后都會(huì)被追加到 _defer 鏈表的表頭,從而設(shè)定了后進(jìn)先出的調(diào)用特性
關(guān)聯(lián)深入理解 Go panic and recover
參考Scheduling In Go
Dive into stack and defer/panic/recover in go
golang-notes
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/31599.html
摘要:恢復(fù)流程如下判斷當(dāng)前中的是否已標(biāo)注為處理從鏈表中刪除已標(biāo)注中止的事件,也就是刪除已經(jīng)被恢復(fù)的事件將相關(guān)需要恢復(fù)的棧幀信息傳遞給方法的參數(shù)每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。 作為一個(gè) gophper,我相信你對(duì)于 panic 和 recover 肯定不陌生,但是你有沒(méi)有想過(guò)。當(dāng)我們執(zhí)行了這兩條語(yǔ)句之后。底層到底發(fā)生了什么事呢?前幾天和同事剛好聊到相關(guān)的話題,發(fā)現(xiàn)其實(shí)大家對(duì)這塊理解還是比較...
摘要:原文地址會(huì)有性能損耗,盡量不要用上個(gè)月在軒脈刃的全棧技術(shù)群里看到一個(gè)小伙伴問(wèn)說(shuō)在棧退出時(shí)執(zhí)行,會(huì)有性能損耗,盡量不要用,這個(gè)怎么解。因此,對(duì)于會(huì)有性能損耗,盡量不能用這個(gè)問(wèn)題,我認(rèn)為該用就用,應(yīng)該及時(shí)關(guān)閉就不要延遲,在用時(shí)一定要想清楚場(chǎng)景。 showImg(https://i.imgur.com/YlKjnSH.jpg); 原文地址:Go defer 會(huì)有性能損耗,盡量不要用? 上個(gè)月...
摘要:為語(yǔ)言提供了強(qiáng)大的協(xié)程編程模式。提供的協(xié)程語(yǔ)法借鑒自,在此向開發(fā)組致敬協(xié)程可以與很好地互補(bǔ)。并發(fā)執(zhí)行使用創(chuàng)建協(xié)程,可以讓和兩個(gè)函數(shù)變成并發(fā)執(zhí)行。協(xié)程需要拿到請(qǐng)求的結(jié)果。 Swoole4為PHP語(yǔ)言提供了強(qiáng)大的CSP協(xié)程編程模式。底層提供了3個(gè)關(guān)鍵詞,可以方便地實(shí)現(xiàn)各類功能。 Swoole4提供的PHP協(xié)程語(yǔ)法借鑒自Golang,在此向GO開發(fā)組致敬 PHP+Swoole協(xié)程可以與...
摘要:還有一種情況就是當(dāng)你在一行中寫了多個(gè)語(yǔ)句,也需要使用分號(hào)來(lái)分開由于語(yǔ)言詞法分析器添加分號(hào)的特殊性,所以在有些情況下需要注意你都不應(yīng)該將一個(gè)控制結(jié)構(gòu)或的左大括號(hào)放在下一行。 Go語(yǔ)言中變量的聲明和JavaScript很像,使用var關(guān)鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個(gè)變量 var m int = 10 // 聲明初始化多個(gè)變量 var i, j...
閱讀 2208·2023-04-25 14:56
閱讀 2553·2021-11-16 11:44
閱讀 2749·2021-09-22 15:00
閱讀 1932·2019-08-29 16:55
閱讀 2211·2019-08-29 14:04
閱讀 2335·2019-08-29 11:23
閱讀 3715·2019-08-26 10:46
閱讀 1940·2019-08-22 18:43