go并发日记·避免goroutine泄露/实现协程可控

千里之行,始于足下。

目录

goroutine内部逻辑来触发控制结束goroutine

方式1

方式2

方式3

外部条件触发控制结束goroutine

多个协程,某个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

未完待续!

如有任何问题欢迎讨论!如果解决了你的疑惑,不妨点个赞哦,再会~

发布了196 篇原创文章 · 获赞 156 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/HYZX_9987/article/details/105559568