千里之行,始于足下。
目录
多个协程,某个goroutine逻辑内部报错需要停止时,需要所有goroutine都结束
goroutine内部逻辑来触发控制结束goroutine
实际背景:逻辑报错了需要停止任务并结束goroutine
方式1
一般goroutine+channel方式
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())//一开始只有main线
signCh := make(chan struct{}) // 信号通知&阻塞main线
// 任务A:doSomeThing
go func() {
defer close(signCh)
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
for i := 0; ; i++ {
fmt.Println("任务A进行中:", i)
if i == 2 { // 模拟出现报错或需要停止的情况
signCh <- struct{}{}
return //很重要
}
time.Sleep(time.Second * 1)
}
}()
<-signCh
defer func() {
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine())
fmt.Println("main线退出。")
}()
}
控制台:
1当前运行的goroutine: 1
2当前运行的goroutine: 2
任务A进行中: 0
任务A进行中: 1
任务A进行中: 2
3当前运行的goroutine: 1
main线退出。
Process finished with exit code 0
方式2
基于context+channel,报错时执行cancel()同时停止任务,以信号channel解除阻塞
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())
signCh := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
// 任务A:doSomeThing
go func(c context.Context) {
for {
select {
case <-c.Done():
fmt.Println("任务A停止")
signCh <- struct{}{}
return
default:
for i := 0; ; i++ {
fmt.Println("任务A:", i)
if i == 2 {
cancel()
break
}
time.Sleep(time.Second * 1)
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
}
}
}
}(ctx)
<-signCh
defer func() {
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine()) // 1
}()
}
控制台:
1当前运行的goroutine: 1
任务A: 0
2当前运行的goroutine: 2
任务A: 1
2当前运行的goroutine: 2
任务A: 2
任务A停止
3当前运行的goroutine: 1
Process finished with exit code 0
如果你想写的代码更多一点,看起来更复杂一点,那么也可以瞧瞧方式3~哈哈(小生不才,认为代码的变换和综合运用很有趣)
方式3
基于context包,把for select用烂,多写点代码玩玩
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())
signCh := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
// 任务A:doSomeThing
go func(c context.Context) {
for i := 0; ; i++ {
if i == 2 { // 模拟出现报错或需要停止的情况
fmt.Println("任务A准备停止...")
cancel() // 通知所有ctx参与的goroutine要结束了
break // 停止任务
}
fmt.Println("任务A进行中:", i)
time.Sleep(time.Second * 1)
}
}(ctx)
go func(c context.Context) { // 专门用来停止
for {
select {
case <-c.Done():
fmt.Println("任务A停止。所有goroutine已收到结束通知,准备结束...")
signCh <- struct{}{} // 发送信号,解除阻塞main线
return
default:
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
time.Sleep(time.Second * 2)
}
}
}(ctx)
//停止for-select循环方式也可移步https://lan6193.blog.csdn.net/article/details/101208252
stop := false
for !stop {
select {
case <-signCh:
stop = true
default:
}
}
defer func() {
time.Sleep(time.Second * 2)
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine()) // 1
}()
}
控制台:
1当前运行的goroutine: 1
任务A进行中: 0
2当前运行的goroutine: 3
任务A进行中: 1
2当前运行的goroutine: 3
任务A准备停止...
任务A停止。所有goroutine已收到结束通知,准备结束...
3当前运行的goroutine: 1
Process finished with exit code 0
外部条件触发控制结束goroutine
方式:使用context的cancel函数
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())
ctx, cancel := context.WithCancel(context.Background())
// 创建goroutine,传入ctx
go func(c context.Context) {
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
for {
select {
case <-c.Done():
fmt.Println("任务A结束。")
return
default:
fmt.Println(" 任务A进行中...")
time.Sleep(time.Second * 1)
}
}
}(ctx)
time.Sleep(time.Second * 3)
fmt.Println("此处暂以延迟3秒来模拟需要停止goroutine的外部条件,准备停止:")
cancel()
fmt.Println("------------")
defer func() {
// 经本地测试,goroutine退出需要一定时间,为两秒时上面的goroutine退出成功。
time.Sleep(time.Second * 2)
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine()) // 1
}()
}
控制台:
1当前运行的goroutine: 1
2当前运行的goroutine: 2
任务A进行中...
任务A进行中...
任务A进行中...
此处暂以延迟3秒来模拟需要停止goroutine的外部条件,准备停止:
------------
任务A结束。
3当前运行的goroutine: 1
Process finished with exit code 0
多个协程,某个goroutine逻辑内部报错需要停止时,需要所有goroutine都结束
多个协程,有一个goroutine中的逻辑报错或需要停止,需要所有goroutine都结束
// 多个协程,有一个goroutine中的逻辑报错或需要停止,需要所有goroutine都结束
func workM(ctx context.Context, ch chan struct{}, f context.CancelFunc) {
str := []string{"A", "B", "C"} //纯粹为了区分三个协程:A、B、C三个任务分别对应自己的协程
for i := range str {
go func(name string, c context.Context) {
for {
select {
case <-c.Done():
fmt.Println("协程" + name + "结束。")
return
default:
fmt.Println("任务\t" + name + "\t正在执行...")
time.Sleep(time.Second * 1)
//模拟协程B开始执行B任务2秒后因内部逻辑报错,需要停止
if name == "B" {
time.Sleep(time.Second * 2)
fmt.Println("B出错了,都准备给我停下!")
f()
ch <- struct{}{}
}
}
}
}(str[i], ctx)
}
}
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())
signCh := make(chan struct{}) // 用来阻塞main线
ctx, cancel := context.WithCancel(context.Background())
workM(ctx, signCh, cancel)
stop := false
for !stop {
select {
case <-signCh:
stop = true
default:
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
time.Sleep(time.Second * 3)
}
}
defer func() {
time.Sleep(time.Second * 1)
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine()) // 1
}()
}
控制台:
1当前运行的goroutine: 1
2当前运行的goroutine: 4
任务 C 正在执行...
任务 B 正在执行...
任务 A 正在执行...
任务 A 正在执行...
任务 C 正在执行...
任务 C 正在执行...
任务 A 正在执行...
2当前运行的goroutine: 4
B出错了,都准备给我停下!
协程A结束。
协程C结束。
协程B结束。
3当前运行的goroutine: 1
Process finished with exit code 0
该需求的另外一种转移解决方式:其中某一个协程内部逻辑报错时往ch中发消息,将cancel()放在外部,select 中监控到ch中的值后触发执行cancel(),因此其他协程读到ctx.Done的值即可退出,相当于内-->外-->内,也能达到所有协程都退出的目的。
多个协程,其中某协程需要停止时不影响其他协程
背景需求:多个协程,有一个goroutine中的逻辑报错或需要停止,其他goroutine仍然正常运行,只结束报错的这个goroutine
假设有A、B、C三个协程,需求是C报错时只结束C协程,A和B依旧正常
func workAA(ctxP context.Context, name string, cancelP context.CancelFunc) {
for {
select {
case <-ctxP.Done():
fmt.Println("协程" + name + "结束。")
return
default:
for i := 0; ; i++ {
fmt.Println("任务"+name+"进行中:", i)
time.Sleep(time.Second * 1)
//模拟协程A内部逻辑报错了,需要A、B一起停止;这里让A(其中之一都可以)先报错
//if i == 6 {
// fmt.Println("任务" + name + "结束。")
// cancelP() // 通知协程A、B都要结束掉
// break
//}
}
}
}
}
func workBB(ctxP context.Context, name string, cancelP context.CancelFunc) {
for {
select {
case <-ctxP.Done():
fmt.Println("协程" + name + "结束。")
return
default:
for i := 0; ; i++ {
fmt.Println("任务"+name+"进行中:", i)
time.Sleep(time.Second * 1)
fmt.Println("2当前运行的goroutine: ", runtime.NumGoroutine())
//if i == 8 { //实际情况时即可按err != nil判断
// fmt.Println("任务" + name + "结束。")
// cancelP() // 通知协程A、B都要结束掉
// break
//}
}
}
}
}
func workCC(ctxChild context.Context, name string, cancelC context.CancelFunc) {
for {
select {
case <-ctxChild.Done():
fmt.Println("协程" + name + "结束。")
return
default:
for i := 0; ; i++ {
fmt.Println("任务"+name+"进行中:", i)
time.Sleep(time.Second * 1)
if i == 2 { //实际情况时即可按err != nil判断
fmt.Println("任务" + name + "结束。")
cancelC() // 结束协程C
break
}
}
}
}
}
func main() {
fmt.Println("1当前运行的goroutine: ", runtime.NumGoroutine())
ctxParent, cancelP := context.WithCancel(context.Background()) // 根上下文
ctxChild, cancelC := context.WithCancel(ctxParent) // 派生一个子ctx
go workAA(ctxParent, "A", cancelP) //该goroutine对应为A协程
go workBB(ctxParent, "B", cancelP) //该goroutine对应为B协程
go workCC(ctxChild, "C", cancelC) //该goroutine对应为C协程
time.Sleep(time.Second * 8) // 用延迟来更好观察A、B是否也结束了
defer func() {
fmt.Println("3当前运行的goroutine: ", runtime.NumGoroutine()) // 为3是正常的
}()
}
控制台:
1当前运行的goroutine: 1
任务C进行中: 0
任务A进行中: 0
任务B进行中: 0
任务A进行中: 1
2当前运行的goroutine: 4
任务B进行中: 1
任务C进行中: 1
2当前运行的goroutine: 4
任务B进行中: 2
任务A进行中: 2
任务C进行中: 2
任务C结束。
协程C结束。
2当前运行的goroutine: 4
任务A进行中: 3
任务B进行中: 3
2当前运行的goroutine: 3
任务B进行中: 4
任务A进行中: 4
任务A进行中: 5
2当前运行的goroutine: 3
任务B进行中: 5
2当前运行的goroutine: 3
任务B进行中: 6
任务A进行中: 6
2当前运行的goroutine: 3
任务B进行中: 7
任务A进行中: 7
3当前运行的goroutine: 3
Process finished with exit code 0
未完待续!
如有任何问题欢迎讨论!如果解决了你的疑惑,不妨点个赞哦,再会~