并发实践中的思考

为什么要写这篇博客

最近在学习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的值等。

总结

  • 写并发程序时,首先要选取好合适的并发方案,不能为了并发而并发;
  • 并发程序不能太相信结果(协程执行顺序不确定),而是要想清楚代码执行的流程(道理)
  • 要加强自己对代码规范的要求
  • 要善用测试,这个以后再补充吧。。

你可能感兴趣的:(并发实践中的思考)