golang源碼分析:defer流程分析

defer

defer是golang中使用的延遲調用的函數,該函數的使用場景就是如果函數執行出錯(panic),也能夠通過recover方式進行捕捉錯誤並將出錯時的一些資源進行回收,如果在性能有要求的情況,並且錯誤能夠控制的情況下還是直接避免使用該函數。

defer的使用場景描述

最理想情況下defer的性能對比
package main

import (
	"testing"
)

func test_defer(){
	defer func(){}()
}

func test_normal(){
  func(){}()
}

func BenchmarkNoDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test_normal()
	}
}

func BenchmarkDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test_defer()
	}
}

對該段代碼進行基準測試;

go test -bench=. deferw_test.go 
goos: darwin
goarch: amd64
BenchmarkNoDefer-4      2000000000               1.21 ns/op
BenchmarkDefer-4        30000000                40.4 ns/op
PASS
ok      command-line-arguments  3.797s

發現如果該函數什麼也不做的話,調用defer的情況比不調用的情況會有性能上的差距較大。

常用的文件操作defer基準測試
package main

import (
	"fmt"
	"os"
	"testing"
)

func test_defer(){
	file, err := os.Open("./test.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()

}

func test_normal(){
	file, err := os.Open("./test.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	
	file.Close()
}

func BenchmarkNoDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test_normal()
	}
}

func BenchmarkDefer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		test_defer()
	}
}

執行基準測試;

go test -bench=. deferw_test.go 
goos: darwin
goarch: amd64
BenchmarkNoDefer-4        100000             17422 ns/op
BenchmarkDefer-4          100000             16553 ns/op
PASS
ok      command-line-arguments  3.789s

此時通過基準測試的輸出,發現是否使用defer性能幾乎相當,在此情況下,其實大部分的性能消耗都位於Open和Close的操作,使得defer在執行的過程中的性能佔比很小几乎對整體性能沒有影響。

defer捕獲panic
package main

import (
	"fmt"
)

func test(){
	defer func(){
		if error := recover(); error != nil {
			fmt.Println("error ", error)
		}
	}()

	panic("raise error ")
}

func main() {
	test()
	fmt.Println("over")
}

此時,輸出的結果如下;

error  raise error 
over

當不加defer中的recover時,此時程序就會報錯退出,如果此時還有些需要回收的資源則不能釋放,並且recover可以保證所在的協程能夠繼續運行下去。

defer的使用思考

defer的使用還是需要考慮到是否需要資源的回收,是否需要從異常中恢復或保存信息來做選擇,如果在高併發的業務場景下並且當前場景下沒有其他的耗時操作則可以考慮選擇不用defer,一般在平常的場景下看個人的喜好來選擇。

defer的執行過程

爲什麼在只有defer的操作過程中(本文第一個示例代碼),添加了defer的操作性能會小於不添加defer的函數呢?接下來查看一下defer的背後到底做了什麼工作。

示例代碼
package main

import "fmt"


func main() {
	defer func(){
		fmt.Println("defer")
	}()

}

進行反編譯之後獲取的指令如下;

  deferw.go:6           0x1092ee0               65488b0c2530000000      MOVQ GS:0x30, CX                        
  deferw.go:6           0x1092ee9               483b6110                CMPQ 0x10(CX), SP                       
  deferw.go:6           0x1092eed               764a                    JBE 0x1092f39                           
  deferw.go:6           0x1092eef               4883ec18                SUBQ $0x18, SP                          
  deferw.go:6           0x1092ef3               48896c2410              MOVQ BP, 0x10(SP)                       
  deferw.go:6           0x1092ef8               488d6c2410              LEAQ 0x10(SP), BP                       
  deferw.go:7           0x1092efd               c7042400000000          MOVL $0x0, 0(SP)                        
  deferw.go:7           0x1092f04               488d05cd9d0300          LEAQ go.func.*+125(SB), AX              
  deferw.go:7           0x1092f0b               4889442408              MOVQ AX, 0x8(SP)                        
  deferw.go:7           0x1092f10               e8eb3bf9ff              CALL runtime.deferproc(SB)              
  deferw.go:7           0x1092f15               85c0                    TESTL AX, AX                            
  deferw.go:7           0x1092f17               7510                    JNE 0x1092f29                           
  deferw.go:11          0x1092f19               90                      NOPL                                    
  deferw.go:11          0x1092f1a               e87144f9ff              CALL runtime.deferreturn(SB)            
  deferw.go:11          0x1092f1f               488b6c2410              MOVQ 0x10(SP), BP                       
  deferw.go:11          0x1092f24               4883c418                ADDQ $0x18, SP                          
  deferw.go:11          0x1092f28               c3                      RET                                     
  deferw.go:7           0x1092f29               90                      NOPL                                    
  deferw.go:7           0x1092f2a               e86144f9ff              CALL runtime.deferreturn(SB)            
  deferw.go:7           0x1092f2f               488b6c2410              MOVQ 0x10(SP), BP                       
  deferw.go:7           0x1092f34               4883c418                ADDQ $0x18, SP                          
  deferw.go:7           0x1092f38               c3                      RET                                     
  deferw.go:6           0x1092f39               e872c2fbff              CALL runtime.morestack_noctxt(SB)       
  deferw.go:6           0x1092f3e               eba0                    JMP main.main(SB) 

從執行的流程可知首先會調用deferproc來創建defer;

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
	if getg().m.curg != getg() {
		// 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)            // 生成一個新的defer結構
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
	d.fn = fn                     // 設置該接口的執行函數
	d.pc = callerpc 							// 調用的pc地址
	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.
}

其中主要就是通過newdefer來創建一個defer,

//go:nosplit
func newdefer(siz int32) *_defer {
	var d *_defer
	sc := deferclass(uintptr(siz))
	gp := getg() 											// 獲取當前的協程
	if sc < uintptr(len(p{}.deferpool)) {     // 是否小於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)           // 複用defer
				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
					d.link = nil
					pp.deferpool[sc] = append(pp.deferpool[sc], d)  // 將當前的d 添加到deferpool中
				}
				unlock(&sched.deferlock)
			})
		}
		if n := len(pp.deferpool[sc]); n > 0 {       // 重新初始化deferpool隊列
			d = pp.deferpool[sc][n-1]
			pp.deferpool[sc][n-1] = nil
			pp.deferpool[sc] = pp.deferpool[sc][:n-1]
		}
	}
	if d == nil { 																// 如果沒有找到則創建一個新的defer
		// Allocate new defer+args.
		systemstack(func() {
			total := roundupsize(totaldefersize(uintptr(siz)))
			d = (*_defer)(mallocgc(total, deferType, true)) 		// 申請內存獲取d的空間大小
		})
		if debugCachedWork {
			// Duplicate the tail below so if there's a
			// crash in checkPut we can tell if d was just
			// allocated or came from the pool.
			d.siz = siz
			d.link = gp._defer 																 // 將協程的_defer保存到d的link上
			gp._defer = d 																		 // 設置新的_defer爲當前的d
			return d
		}
	}
	d.siz = siz
	d.link = gp._defer
	gp._defer = d
	return d
}

從新建的流程可知,通過協程的_defer來保存該協程中所有的_defer,如果有新增則將新增的添加到頭部,通過這麼一個鏈表來完成defer的先入後執行。

當執行到runtime.deferreturn時,就會觸發defer的執行。

//go:nosplit
func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer  			// 獲取當前協程的_defer鏈表
	if d == nil {
		return 							// 如果爲空則返回
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}

	// Moving arguments around.
	//
	// Everything called after this point must be recursively
	// nosplit because the garbage collector won't know the form
	// of the arguments until the jmpdefer can flip the PC over to
	// fn.
	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 								// 獲取當前的fn
	d.fn = nil 								// 將當前fn置空
	gp._defer = d.link  			// 獲取下一個_defer並設置到_defer中
	freedefer(d) 							// 釋放內容 
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))   // 執行fn函數
}

至此,defer的函數的執行過程就大致執行完成。

總結

defer的使用場景根據需要自行選擇,如果在高併發的情況下,還是儘量少使用defer的調用,如果在有出錯的情況下且有其他資源需要管理的情況下,建議使用defer來控制資源的回收或釋放,並且defer的執行鏈路都是通過協程來執行的,所以defer執行的過程中要注意是否跨了協程來操作了其他的資源,可能會達不到defer先入後出的效果。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章