在多协程并发环境下,我们常常会碰到以下两个问题。假设我们现在有 2 个协程,我们叫它们协程 A 和 B 。
【问题1】如果协程 A 发生了panic
,协程 B 是否会因为协程 A 的 panic
而挂掉?【问题2】如果协程 A 发生了 panic
,协程 B 是否能用 recover
捕获到协程 A 的 panic
?
答案分别是:会、不能。
1.【问题1】 【问题1】如果协程 A 发生了panic
,协程 B 是否会因为协程 A 的 panic
而挂掉?
package main
import (
"fmt"
"time"
)
func main() {
// 协程 A
go func() {
for {
fmt.Println("协程 A")
}
}()
// 协程 B
go func() {
time.Sleep(1 * time.Microsecond) // 确保 协程 A 先运行起来
panic("协程 B panic")
}()
time.Sleep(10 * time.Second) // 充分等待协程 B 触发 panic 完成和协程 A 执行完毕
fmt.Println("main end")
}
输出结果:
协程 A
协程 A
协程 A
协程 A
协程 A
协程 A
协程 A
协程 A
协程 A
协程 A
panic: 协程 B panic
goroutine 6 [running]:
main.main.func2()
/home/wohu/GoCode/src/hello.go:19 +0x46
created by main.main
/home/wohu/GoCode/src/hello.go:17 +0x51
exit status 2
可以看到,在协程 B 触发 panic 之后,协程 A 并没有继续打印,并且主协程的 main end
也没有打印出来,充分说明了在 B 协程触发 panic 之后,在 A 协程也会因此挂掉,且主协程也会挂掉。
panic
,协程 B 是否能用 recover
捕获到协程 A 的 panic
?
精简上面的代码如下:
package main
import (
"fmt"
"time"
)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("panic err is %s", err)
}
}()
// 协程 B
go func() {
panic("协程 B panic")
}()
time.Sleep(1 * time.Second) // 充分等待协程 B 触发 panic 完成
fmt.Println("main end")
}
我们开启 1 个协程 B,并在主协程中增加 recover
机制,尝试在主协程中捕获协程 B 触发的 panic
, 但是结果未能如愿。 打印结果如下:
panic: 协程 B panic
goroutine 5 [running]:
main.main.func2()
/home/wohu/GoCode/src/hello.go:18 +0x39
created by main.main
/home/wohu/GoCode/src/hello.go:17 +0x59
exit status 2
从结果可以看到, recover
并没有生效,所以我们可以下结论:
哪个协程发生 panic,就需要在哪个协程自身中 recover 。
改成如下代码,可以正常 recover。
package main
import (
"fmt"
"time"
)
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("panic err is %s\n", err)
}
}()
panic("panic")
}()
time.Sleep(1 * time.Second) // 充分等待协程触发 panic 完成
fmt.Println("main end")
}
输出结果:
panic err is panic
main end
所以结论如下:
协程A发生 panic
,协程B无法 recover
到协程A的 panic
,只有协程自己内部的 recover
才能捕获自己抛出的 panic
。
package main
import (
"fmt"
"sync"
"time"
)
// 该函数的参数为多个业务逻辑函数,且函数个数为变长参数
func allTasks(tasks ...func()) {
var wg sync.WaitGroup
for _, t := range tasks {
wg.Add(1) // 每启动一个协程等待组加 1
go func(f func()) { // 匿名函数的参数为业务逻辑函数
defer func() {
// 在每个协程内部接收该协程自身抛出来的 panic
if err := recover(); err != nil {
fmt.Println("defer", err)
}
wg.Done() // 每个协程结束时给 等待组减 1
}()
f() // 业务函数调用执行
}(t) // 将当前的业务函数名传递给协程
}
wg.Wait()
}
// 业务逻辑 A
func A() {
fmt.Println("A func begin")
panic("error A")
}
// 业务逻辑 B
func B() {
fmt.Println("B func begin")
}
func main() {
allTasks(A, B) // 将业务逻辑函数名 A B 传递给封装好的处理函数
time.Sleep(1 * time.Second)
fmt.Println("main end")
}
输出结果
B func begin
A func begin
defer error A
main end
这样我们就实现了一个通用的并发处理逻辑,每次调用我们只需要把业务逻辑的函数传入即可,不用每次自己单独编写一套并发控制逻辑;同时调用逻辑 B 就不会因为调用逻辑 A 的 panic
而挂掉了,容错率更高。在业务开发中我们可以参考这种实现方式。
package main
import (
"fmt"
"sync"
)
var wg sync.Waitgroup
func div(num int) {
defer func() {
err := recover()
if err != nil {
fmt.Println(err) }
wg.Done()
}()
fmt.Printf("10/%d=%d\n", num, 10/num)
}
func main() {
for i:=0; i<10;i++ {
wg.Add(i)
go div(i)
}
wg.Wait()
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)