谨慎使用Goroutine

642 阅读3分钟

背景

随便使用Goroutine如同随地大小x,使用Goutine时愈发觉得没有安全感,这种感觉延伸到使用三方依赖

为什么这么说?源于Go针对panic的处理方式,直接看代码:

func main() {
	fmt.Println("main running...")

	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("%v\n", err)
		}
	}()

	go doSomething()

	time.Sleep(5 * time.Second)
	fmt.Println("main end...")
}

func doSomething() {
	fmt.Println("do something...")
	panic("do something panic")
}

main中的recover是否能捕获到doSomething中的panic呢,答案是不行,然后呢?然后程序就崩溃了,只能在doSomething中recover,例如这样:

func doSomething() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("%v\n", err)
		}
	}()
	fmt.Println("do something...")
	panic("do something panic")
}

如果调用的方法使用了goroutine而它可能存在panic的风险我们该如何避免呢,倘若这个方法自己可以修改那就到goroutine方法中增加recover,如果不能修改呢,例如第三方依赖,那就自求多福吧

深入探究

go build -gcflags=-S .\main.go

使用上面的命令查看汇编代码,着重查看panic和recover

...
0x0067 00103 (/main.go:17)        LEAQ    main.doSomething·f(SB), AX
0x006e 00110 (/main.go:17)        CALL    runtime.newproc(SB)
...

调用runtime.newproc创建goroutine

recover
...
0x000e 00014 (/main.go:25)        LEAQ    +88(FP), AX
0x0013 00019 (/main.go:25)        PCDATA  $1, $0
0x0013 00019 (/main.go:25)        CALL    runtime.gorecover(SB)
0x0018 00024 (/main.go:25)        TESTQ   AX, AX
0x001b 00027 (/main.go:25)        JEQ     84
...

recover实际上为runtime.gorecover,我们跟踪下这个方法,在runtime/panic.go中

func gorecover(argp uintptr) any {
	// 获取当前goroutine
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

发现recover方法很简洁:获取当前goroutine然后将_panic结构体的recovered字段标记为true

panic
...
0x0065 00101 (/main.go:30)        LEAQ    type:string(SB), AX
0x006c 00108 (/main.go:30)        LEAQ    main..stmp_3(SB), BX
0x0073 00115 (/main.go:30)        CALL    runtime.gopanic(SB)
0x0078 00120 (/main.go:30)        XCHGL   AX, AX
0x0079 00121 (/main.go:30)        CALL    runtime.deferreturn(SB)
0x007e 00126 (/main.go:30)        ADDQ    $72, SP
0x0082 00130 (/main.go:30)        POPQ    BP
0x0083 00131 (/main.go:30)        RET
0x0084 00132 (/main.go:30)        NOP
...

可以看到panic其实是runtime.gopanic,跟踪到runtime包下的gopanic方法,在runtime/panic.go中


//go:linkname gopanic
func gopanic(e any) {
	...
	// 获取当前goroutine
	gp := getg()
	...

	// 执行所有defer
	for {
		fn, ok := p.nextDefer()
		if !ok {
			break
		}
		fn()
	}

	...
	// 关键所在
	fatalpanic(&p)   // should not return
	*(*int)(nil) = 0 // not reached
}

继续看下defer方法:nextDefer

func (p *_panic) nextDefer() (func(), bool) {
	gp := getg()

	...
	if p.recovered {
		mcall(recovery)
		...
	}

	...
}

如果当前goroutine的_painc中recovered为true执行mcall,熟悉GMP模型应该知道mcall就是放弃执行权交还给g0,恢复时执行recovery函数

mcall switches from the g to the g0 stack and invokes fn(g)

从这里可以看出gorecover设置recovered的作用了,ok,我们找到了recover然后程序继续运行原因了,如果我们没有recover呢?

看下fatalpanic

//go:nosplit
func fatalpanic(msgs *_panic) {
	...
	gp := getg()
	...

	systemstack(func() {
		exit(2)
	})

	*(*int)(nil) = 0 // not reached
}

exit ?

func exit(code int32) {
	...
	stdcall1(_ExitProcess, uintptr(code))
}

ExitProcess ? 是的,直接结束进程

小结
1. recover只在当前goroutine有效
2. 任何一个goroutine中的panic都会导致整个程序退出

总结

虽然Go中提供了GMP模型更轻量级的并发能力,在编程方面使用go关键字更方便开启goroutine,但基于上述原因非必要不使用,不乱用,使用时考虑增加recover防止程序崩溃