1. 问题现象
如果在开发过程中不考虑 goroutine
在什么时候能退出和控制 goroutine
生命期,就会造成 goroutine
失控或者泄露的情况 ,看示例代码:
func consumer(ch chan int) {
for {
data := <-ch
fmt.Println(data)
}
}
func main() {
ch := make(chan int)
for {
var input string
// 获取输入,模拟进程持续运行
fmt.Scan(&input)
go consumer(ch)
// 输出现在的goroutine数量
fmt.Println("goroutines 个数为:", runtime.NumGoroutine())
}
}
运行程序,每输入一个字符串+回车,将会创建一个 goroutine
, 结果如下 :
a
goroutines 个数为: 2
b
goroutines 个数为: 3
c
goroutines 个数为: 4
d
goroutines 个数为: 5
e
goroutines 个数为: 6
f
goroutines 个数为: 7
g
goroutines 个数为: 8
h
goroutines 个数为: 9
i
goroutines 个数为: 10
上面代码模拟一个进程根据需要创建 goroutine
的情况 。我们发现, 随着输入的字符串越来越多, goroutine
将会无限制地被创建,但并不会结束,因为 consumer
是个阻塞操作,且 channel
中没有让其退出的操作。如果一直持续下去将会造成内存大量分配,最终使进程崩溃。
那要如何解决呢?
2. 解决方案
2.1 创建单个子协程,在子协程中处理业务
修改上面代码,将创建子协程的代码挪动到循环外面。
func consumer(ch chan int) {
for {
data := <-ch
fmt.Println("data is: ", data)
}
}
func main() {
ch := make(chan int)
go consumer(ch) // 在循环外面创建协程
for {
var input string
fmt.Scan(&input)
fmt.Println("goroutines 个数为:", runtime.NumGoroutine())
}
}
这样输出结果为:
a
goroutines 个数为: 2
v
goroutines 个数为: 2
x
goroutines 个数为: 2
w
goroutines 个数为: 2
f
goroutines 个数为: 2
g
goroutines 个数为: 2
b
goroutines 个数为: 2
从结果可以看到协程数量并没有随着收入字符的增多而增加,但是存在一个问题就是,子协程并没有退出的机制。
如何解决呢?接着往下看
2.2 设置子协程退出条件
在主协程中设置当输入字符串为 quit
时,往通道里面写入 -1,子协程从通道里面获取数据为 -1 时就退出。此时主协程仍然是有效的,但是子协程会永远退出,所以协程数量为 1 。
func consumer(ch chan int) {
for {
data := <-ch
// 收到的数据为 -1 时,退出该循环,同时也会退出该协程
if data == -1 {
break
}
fmt.Println("data is: ", data)
}
}
func main() {
ch := make(chan int)
go consumer(ch) // 在循环外面创建协程
for {
var input string
fmt.Scan(&input)
if input == "quit" {
ch <- -1 // 当输入为 quit 时,往通道里面写入 -1
}
// 输出现在的goroutine数量
fmt.Println("goroutines 个数为:", runtime.NumGoroutine())
}
}
输出结果:
a
goroutines 个数为: 2
b
goroutines 个数为: 2
c
goroutines 个数为: 2
d
goroutines 个数为: 2
quit
goroutines 个数为: 2
a
goroutines 个数为: 1
b
goroutines 个数为: 1
3. 总结
从上面示例我们可以总结使用协程的一般原则:
- 尽量避免无限制的创建协程;
- 在需要反复创建协程的场景下,协程一定要有退出的条件,并且确保该退出条件能满足(即代码能执行到);