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先入后出的效果。由于本人才疏学浅,如有错误请批评指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值