阅读 383
Goroutine实现并发应该思考的问题

Goroutine实现并发应该思考的问题

摘要

本文主要介绍使用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/…

写在最后

本篇文章仅作为笔者学习实践过程中的一些总结记录,有任何不妥或者不正确的地方欢迎批评指正

文章分类
后端
文章标签