当前位置: 首页 > 科技观察

Go多协程并发环境下的错误处理

时间:2023-03-17 15:57:44 科技观察

介绍在Go语言中,我们通常使用panic和recover来抛出和捕获错误。这对操作在单协程环境下可以正常使用,也不会踩到任何坑。但是在多协程的并发环境中,我们经常会遇到以下两个问题。假设我们现在有2个协程,姑且称它们为协程A和协程B:如果协程Apanic了,协程B会不会因为协程A的panic而挂掉?如果协程A发生panic,协程B是否可以使用recover来捕获协程A的panic?答案是:是的,不是的。那我们就一一验证,给出具体业务场景下的最佳实践。问题1如果协程Apanic,协程B会不会因为协程A的panic而挂掉?为了验证这个问题,我们编写了一个程序:{time.Sleep(1*time.Second)panic("goroutine2_panic")}()time.Sleep(2*time.Second)}首先,主协程启动两个子协程A和B,A协程打印goroutine1_print连续打印字符串;B协程休眠1s后抛出panic(sleep步骤是为了保证A运行并开始打印后B会panic),主协程休眠2s,等待A、B子协程结束执行,主协程退出。最终打印结果如下:...goroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printpanic:goroutine2_panicgoroutine1_printgoroutine1_printgoroutinegoroutine1_print19goroutine1_printgoroutine1_printgoroutine1_printgoroutine1_print[runninggoroutine1_print]:goroutine1_printgoroutine1_printgoroutine1_printmain.main.func2()/Users/jiangbaiyan/go/src/awesomeProject/main.go:18+0x46createdbymain.main/Users/jiangbaiyan/go/src/awesomeProject/main.go:16+0x4d我们可以看到在协程Bpanic之前,协程A一直在打印字符串;然后协程A和panicprintstrings交替,最后主协程和协程A和B都退出。所以我们可以看到,一个协程panic后,所有的协程都会挂掉,程序会整体退出。到这里我们已经验证了第一个问题的答案。至于panic和coroutineA交替打印的原因,可能是因为panic也需要打印字符串。因为打印也是需要时间的,当我们执行panic这行代码的时候,到panic真正触发所有协程挂掉需要一定的时间(虽然这个时间很短),所以在这短短的时间内,我们将看到交替打印。问题2如果协程A发生panic,其他协程是否可以使用recover捕获协程A的panic?或者类似上面的代码,我们可以进一步简化:}}()gofunc(){panic("goroutine2_panic")}()time.Sleep(2*time.Second)}这次我们只启动了一个协程,并在主协程中添加了recover,希望它能捕捉到panic在子协程中,但结果失败:panic:goroutine2_panicgoroutine6[running]:main。main.func2()/Users/jiangbaiyan/go/src/awesomeProject/main.go:17+0x39createdbymain.main/Users/jiangbaiyan/go/src/awesomeProject/main.go:16+0x57Processfinishedwithexitcode2我们看到recover没有走影响。所以哪个coroutinepanic发生了,我们需要在哪个coroutine中恢复,我们改成这样:!=nil{fmt.Println("recover_panic")}}()panic("goroutine2_panic")}()time.Sleep(2*time.Second)}结果成功打印了recover_panicstring:recover_panicProcessfinishedwithexitcode0所以我们的答案也得到了验证:协程A发生panic,协程B无法恢复协程A的panic,只有协程自身内部的recover才能捕捉到自己抛出的panic。最佳实践我们假设有这样一个场景,我们要开发一个客户端,这个客户端需要调用2个服务,这2个服务没有任何顺序依赖,所以我们可以开启2个goroutine,并发调用这两个服务获得性能提升。那么这个时候我们刚才讲的问题就变成了问题。一般来说,我们不希望其中一个服务调用失败,另一个服务调用也失败,而是继续执行其他几个服务调用的逻辑。这个时候怎么办?聪明的你肯定会想,我在每个协程里面写一个recover语句,让他捕捉每个协程可能发生的panic,解决协程panic导致所有协程挂掉的问题。我们编写如下代码,是业务开发中结合问题2解决问题1的最佳实践://并发调用服务,每个handler会传入一个调用逻辑函数funcGoroutineNotPanic(handlers...func()error)(errerror){varwgsync.WaitGroup//假设我们要调用处理程序那么多服务for_,f:=rangehandlers{wg.Add(1)//每个函数启动一个协程gofunc(handlerfunc()error){deferfunc(){//每个协程内部使用recover来捕获调用逻辑中可能出现的panic:=recover();e!=nil{//某个服务调用协程报错,可以在这里打印一些错误日志}wg.Done()}()//走第一个errorhandler调用逻辑,最后返回e:=handler()iferr==nil&&e!=nil{err=e}}(f)}wg.Wait()return}以上方法调用示例://调用示例funcmain(){//调用逻辑1aRpc:=func()error{panic("rpclogicApanic")returnnil}//调用逻辑2bRpc:=func()error{fmt.Println("rpclogicB")returnnil}err:=GoroutineNotPanic(aRpc,bRpc)iferr!=nil{fmt.Println(err)}}这样我们就实现了一个普通的并发处理逻辑,我们只需要把业务逻辑的函数传入即可,没有每次都需要单独写一套并发控制逻辑;同时,调用逻辑2不会因为调用逻辑1的panic而挂掉,容错率更高。我们在业务开发中可以参考这个实现方法~本文转载自微信公众号「NoSay」,可以通过以下二维码关注。转载请联系NoSay公众号。