为什么要写这篇博客
最近在学习Go并发的时候,想写一个并发的求斐波那契数的程序。期间遇到了一些坑,所以来记录一下自己的想法。
写并发程序中遇到的坑
并发1.0
使用最原始的方式,每次递归的时候,开一个协程去跑,将结果放入channel中。最终发现在求的数比较大的时候,并发比单线程还要慢。
并发2.0
于是我考虑应该是开的协程太多了,使用协程池来控制协程数量。代码如下:
func mutiExample(pool *tunny.Pool, ch chan int64, n int64) {
var ans int64
switch n {
case 0:
ans = 0
case 1, 2:
ans = 1
default:
res := make(chan int64, 2)
a := func() {
fmt.Println("start ", n-1)
mutiExample(pool, res, n-1)
}
b := func() {
fmt.Println("end ", n-2)
mutiExample(pool, res, n-2)
}
go pool.Process(a)
go pool.Process(b)
ans = <-res + <-res
}
ch <- ans
}
func main() {
start := time.Now()
pool2 := tunny.NewCallback(3)
ch := make(chan int64, 1)
mutiExample(pool2, ch, 5)
fmt.Println(<-ch, time.Since(start))
}
发现的问题
现象
- 如果求的值为5,协程数量为3,一定概率会死锁;
- 如果求的值为5,协程数量为4,则不会死锁;
分析
我们来通过一组结果分析它的执行过程
start 4
end 3
end 1
end 2
start 3
- 显然,程序先开了两个协程去跑 n=4 和 n=3 的情况,由于未获取全部返回,协程会一直阻塞;
- 然后是 n=2 或 n=1 的情况,将结果写入channel,并结束;
-
最后执行 n=3 的情况,由于3个协程全部被占用,且它们所期望的值无法返回,会造成死锁。
流程.png
结论
如果要让程序不出现死锁,则需要限定协程池大小大于执行 n>2 的协程的总数量。
因为这种情况下程序无法直接返回,在等待接收数据。
自己的一些思考
- 在当时遇到这个问题的时候,自己一直在想是不是自己程序出了问题,而没有对程序的执行流程进行分析。所以也是给自己提了个醒,在写程序的时候,要提前能想清楚程序的执行流程,不要盲目的依赖程序的运行结果。
- 你会发现2.0程序的执行效率还是很低,可以发现这种递归程序,盲目的去开协程,是不会对效率有提升的。因为当递归层数很多时,协程池在调度协程等问题上会花费很多的时间。
- 贴出代码的原因是因为自己的编码规范实在太差,比如:命名,取channel的值等。
总结
- 写并发程序时,首先要选取好合适的并发方案,不能为了并发而并发;
- 并发程序不能太相信结果(协程执行顺序不确定),而是要想清楚代码执行的流程(道理)
- 要加强自己对代码规范的要求
- 要善用测试,这个以后再补充吧。。
网友评论