Golang的關鍵字defer的使用方法
核心思想
在defer出現的地方插入瞭指令CALL runtime.deferproc,在函數返回的地方插入瞭CALL runtime.deferreturn。goroutine的控制結構中,有一張表記錄defer,調用runtime.deferproc時會將需要defer的表達式記錄在表中,而在調用runtime.deferreturn的時候,則會依次從defer表中“出棧”並執行
如果有多個defer,調用順序類似棧,越後面的defer表達式越先被調用
defer鏈
defer信息會註冊到鏈表,當前執行的 goroutine 持有這個鏈表的頭指針,每個 goroutine 都有一個對應的結構體struct G,其中有一個字段指向這個defer鏈表頭
type g struct { // Stack parameters. // stack describes the actual stack memory: [stack.lo, stack.hi). // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. // stackguard1 is the stack pointer compared in the C stack growth prologue. // It is stack.lo+StackGuard on g0 and gsignal stacks. // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). stack stack // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink // _defer 這個字段指向defer鏈表頭 _defer *_defer // innermost defer ... }
新註冊的defer會添加到鏈表頭,所以感覺像是棧那樣先進後出的調用:
源碼分析
deferproc一共有兩個參數,第一個是參數和返回值的大小,第二個是指向funcval的指針
// 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 // 獲取當前goroutine 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() // 創建defer結構體 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. }
// 以下是_defer結構體 // 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 記錄defer的參數和返回值共占多少字節 // 會直接分配在_defer後面,在註冊時保存參數,在執行完成時拷貝到調用者參數和返回值空間 siz int32 // includes both arguments and results // started 標記是否已經執行 started bool // heap go1.13優化,標識是否為堆分配 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 是否是open defer,通過這些信息可以找到未註冊到鏈表的defer函數 openDefer bool // sp 記錄調用者棧指針,可以通過它判斷自己註冊的defer是否已經執行完瞭 sp uintptr // sp at time of defer // pc deferproc的返回地址 pc uintptr // pc at time of defer // fn 要註冊的funcval fn *funcval // can be nil for open-coded defers // _panic 指向當前的panic,表示這個defer是由這個panic觸發的 _panic *_panic // panic that is running defer // link 鏈到前一個註冊的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. // 通過這些信息可以找到未註冊到鏈表的defer函數 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將參數註冊的時候拷貝到堆上,執行時再(將參數和返回值)拷貝回棧上
go會分配不同規格的_defer pool,執行時從空閑_defer中取一個出來用,沒有合適的再進行堆分配。用完以後再放回空閑_defer pool。以避免頻繁的堆分配和回收
優化
go1.12中defer存在的問題:
- defer信息主要存儲在堆上,要在堆和棧上來回拷貝返回值和參數很慢
- defer結構體通過鏈表鏈起來,而鏈表的操作也很慢
go1.13中defer的優化:
- 減少瞭defer信息的堆分配。再通過deferprocStack將整個defer註冊到defer鏈表中
- 將一般情況的defer信息存儲在函數棧幀的局部變量區域
- 顯示循環或者是隱式循環的defer還是需要用到go1.12中defer信息的堆分配
- 官方給出的性能提升是30%
go1.14中defer的優化:
- 在編譯階段插入代碼,把defer函數的執行邏輯展開在所屬函數內,避免創建defer結構體,而且不需要註冊到defer鏈表。稱為 open coded defer
- 與1.13一樣不適用於循環中的defer
- 性能幾乎提升瞭一個數量級
- open coded defer 中發生panic 或 調用runtime.Goexit(),後面未註冊到的defer函數無法執行到,需要棧掃描。defer結構體中就多添加瞭一些字段,借助這些字段可以找到未註冊到鏈表中的defer函數
結果就是defer變快瞭,但是panic變慢瞭
defer添加瞭局部變量去判斷是否需要執行,需要執行的話就將標識df對應的位上或一下,如果是有條件的defer,需要根據具體條件去或df
deferprocStack
// deferprocStack queues a new deferred function with a defer record on the stack. // The defer record must have its siz and fn fields initialized. // All other fields can contain junk. // The defer record must be immediately followed in memory by // the arguments of the defer. // Nosplit because the arguments on the stack won't be scanned // until the defer record is spliced into the gp._defer list. //go:nosplit func deferprocStack(d *_defer) { // 獲得當前 goroutine gp := getg() if gp.m.curg != gp { // go code on the system stack can't defer throw("defer on system stack") } // siz and fn are already set. // The other fields are junk on entry to deferprocStack and // are initialized here. // 初始化 _defer 信息 d.started = false d.heap = false d.openDefer = false d.sp = getcallersp() d.pc = getcallerpc() d.framepc = 0 d.varp = 0 // The lines below implement: // d.panic = nil // d.fd = nil // d.link = gp._defer // gp._defer = d // But without write barriers. The first three are writes to // the stack so they don't need a write barrier, and furthermore // are to uninitialized memory, so they must not use a write barrier. // The fourth write does not require a write barrier because we // explicitly mark all the defer structures, so we don't need to // keep track of pointers to them with a write barrier. *(*uintptr)(unsafe.Pointer(&d._panic)) = 0 *(*uintptr)(unsafe.Pointer(&d.fd)) = 0 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) return0() // No code can go here - the C return register has // been set and must not be clobbered. }
到此這篇關於Golang的關鍵字defer的使用的文章就介紹到這瞭,更多相關 Golang關鍵字defer內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Go defer 原理和源碼剖析(推薦)
- Golang中panic與recover的區別
- go並發編程sync.Cond使用場景及實現原理
- Golang map實現原理深入分析
- Golang的鎖機制與使用技巧小結