摘要
本文主要介绍使用go关键字创建goroutine时可能存在的一些问题,比如由于goroutine一直存在于应用程序的整个生命周期, 占有的内存无法得到释放,从而造成内存泄漏;在程序中开启的goroutine没有做好超时控制,造成接口响应超时问题。这也是 Never start a goroutine without knowing how it will stop背后的一些原因。
常用姿势
1. ch := somefunction()
2. go func() {
3. for range ch { }
4. }()
第1行包含一个channel, 第2~4行开启一个goroutine进行消费。这个goroutine何时退出,当ch被关闭时才会退出。很难判断channel何时被关闭,由此可能造成goroutine无法释放的内存泄漏。如下列代码内存泄漏代码
内存泄漏
1.func leak() {
2. ch := make(chan int)
3. go func() {
4. val := <-ch
5. fmt.Println("We received a value:", val)
6. }()
7.}
leak函数在第2行创建了一个无缓冲的channel, 允许接受一个int类型的整数。第3行开启一个goroutine,第4行等待接受从channel中发送的值。goroutine一直在等待channel中发送过来的值,然而leak函数退出了。再没有信号发送给channel,用于通知匿名的goroutine退出。又一次goroutine会一直存在于应用程序的整个生命周期。资源无法释放,从而造成goroutine类型的内存泄漏
超时控制
1. func search(term string)(string, error){
2. time.Sleep(time.Millisecond * 200)//模拟网络时延
3. return "some value.", nil
4. }
5.
6. func process(term string)error{
7. record, err := search(term)
8. if nil != err {
9. return err
10. }
11. fmt.Println("recieved:" .. record)
12. return nil
13. }
在第5行process函数中,会存在调用其他子函数的情况,有时候search响应会相当耗时,造成整个API响应超时,因此要做超时控制, 接口演变如下
1. type result struct {
2. record string
3. err error
4. }
5.
6. func search(term string) (string, error) {
7. time.Sleep(time.Second * 6)
8. return "some value.", nil
9. }
10.
11. func process(term string) error {
12. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
13. defer cancel()
14. ch := make(chan result) //采用channel进行阻塞,控制
15. go func() {
16. record, err := search(term)
17. ch <- result{record, err}
18. }()
19. select {
20. case <-ctx.Done():
21. return errors.New("the function is cannelled.")
22. case result := <-ch:
23. if result.err != nil {
24. return result.err
25. }
26. fmt.Println("receive:", result.record)
27. return nil
28. }
29. }
定义结构体result用来存放search的返回结果和错误信息,在process函数中定义context,用来做超时控制,通过go关键字将search函数进行异步调用, 采用select等待结果返回,或者超时取消。由此做到了超时控制。并且在发起的goroutine中通过channel控制程序的终止。但是这个例子中仍然存在概率性的goroutine泄漏,即search响应超时,触发context取消。此时即使search结果返回,等待另一个程序消费channel中的数据,但却没哟,由此触发了goroutine泄漏。解决办法为将第14行中对channel定义由无缓冲改为有一个缓冲空间即可,即将第14行代码做如下修改:
ch := make(chan result, 1)
关于channal请看笔者下一次总结
总结
关于golang中go关键字,为开发人员提供了方便的并发编程能力。但是如果不慎重使用会造成无法查找的疯狂的bug.基于此产生了本篇文章。每开启一个goroutine,请思考如下问题,1)该goroutine的调用者是谁,2)该goroutine何时结束,3)该goroutine怎么结束。这些问题将帮忙我们避免goroutine的内存泄漏,做好超时控制。最后在重申一次 Never start a goroutine without knowing how it will stop
参考文档
[1]dave.cheney.net/2016/12/22/…
[2]www.ardanlabs.com/blog/2018/1…
[3]www.ardanlabs.com/blog/2019/0…
[4]www.ardanlabs.com/blog/2014/0…
[5]u.geekbang.org/subject/go/…
写在最后
本篇文章仅作为笔者学习实践过程中的一些总结记录,有任何不妥或者不正确的地方欢迎批评指正