Go 知识点(14) — Go 多协程(单个协程触发panic会导致其它所有协程挂掉,每个协程只能捕获到自己的 panic 不能捕获其它协程)

在多协程并发环境下,我们常常会碰到以下两个问题。假设我们现在有 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 协程也会因此挂掉,且主协程也会挂掉。

2.【问题2】

  • 【问题2】如果协程 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

3. 业务开发实践

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()
}
在Go语言中连接MySQL要获取每个thread线程号,可以使用`SHOW PROCESSLIST`命令查询MySQL当前正在执行的进程,然后获取每个进程的ID即可。 具体实现步骤如下: 1. 导入`database/sql`和`github.com/go-sql-driver/mysql`包。 ```go import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) ``` 2. 使用`sql.Open()`方法连接MySQL数据库。 ```go db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name") if err != nil { panic(err.Error()) } defer db.Close() ``` 3. 执行`SHOW PROCESSLIST`命令查询MySQL当前正在执行的进程。 ```go rows, err := db.Query("SHOW PROCESSLIST") if err != nil { panic(err.Error()) } defer rows.Close() ``` 4. 遍历查询结果,获取每个进程的ID。 ```go for rows.Next() { var id int var user string var host string var db string var command string var time string var state string var info string err = rows.Scan(&id, &user, &host, &db, &command, &time, &state, &info) if err != nil { panic(err.Error()) } fmt.Printf("ID: %d\n", id) } ``` 在遍历查询结果时,可以通过`rows.Scan()`方法获取每个进程的ID和其他信息。其中,ID就是每个线程的线程号。 完整代码示例: ```go package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name") if err != nil { panic(err.Error()) } defer db.Close() rows, err := db.Query("SHOW PROCESSLIST") if err != nil { panic(err.Error()) } defer rows.Close() for rows.Next() { var id int var user string var host string var db string var command string var time string var state string var info string err = rows.Scan(&id, &user, &host, &db, &command, &time, &state, &info) if err != nil { panic(err.Error()) } fmt.Printf("ID: %d\n", id) } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wohu007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值