jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

Golang中goroutine死锁问题

作者:jasper | 分类:Golang | 标签:   | 阅读 431 次 | 发布:2017-06-25 10:45 p.m.

还是接着上文所说,遇到golang的web程序没有响应后,就用了pprof做了监控,结果发现goroutine的数量在一直缓慢地增长,到一定数目之后程序就没有响应了。在此记录一下排查过程,以及解决方案。

定位问题

通过上文提到的web的方式,查看了一下发现最多的goroutine都定位在了一个channel的读的地方,而且可以看到日志中不断地有panic报出来,由于做了recover,所以程序并不会crash,但是触发了panic的请求会hang住,而且每遇到一次会hang住的请求,goroutine就会增加一个。

为了重现这个问题,把程序的主要部分拿出来并简化后,就是这样咯:

func main(){
    channel := make(chan int, 5)
    for i:=0; i<5;i++  {
        go func(j int) {
            defer func(){
                if err:=recover();err!=nil {
                    println("panic recovered!")
                }
            }()
            println(j)
            if(j==2){
                println("make a panic")
                panic(1)
            }
            channel <- i
        }(i)
    }
    for j := 0; j < 5; j++ {
     <-channel
    }
    println("finished!")
}

输出结果:

4
1
0
3
2
make a panic
panic recovered!
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /Users/zhangjian/app/golang/src/github.com/jasper-zhang/go_example/panic_test:22 +0xe1
exit status 2

可以看到没有运行到最后的finish这一步,而且错误定位到了<-channel从channel里面读数据这里,错误提示的很明确all goroutines are asleep - deadlock!也就是goroutine死锁了,查看文档后我明白了,出错的原因就是在main goroutine里期望从channel里面获取到数据,这个数据是由其他的goroutine写进去的,但是其他的goroutine已经执行结束,所以main goroutine在等一个永远不会写入的数据,所以就没必要等了,就给用户报了一个deadlock,而且这里是所以goroutine都结束之后才报出来了,我们做个试验:

    time.Sleep(10*time.Second)
    panic(1)

在panic之前加上了一个sleep 10秒,则程序在10秒之后才报出的deadlock,另外注意这里并不是panic,所以你在main goroutine里加上recover是没有用的,而且在我的web程序里面也没有发现有deadlock报出来,这个我现在还是有些困惑。

那一切是明白了,由于往channel里面写入的goroutine因为panic少写了一个进去,所以接受方死锁了。对于golang的channel分为有缓冲的和无缓冲的channel,如果channel是无缓冲的,接收方会一直阻塞直到有数据到来。发送方会一直阻塞直到接收方将数据取走。如果channel带有缓冲区,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。

所以不管是写入还是读取都会遇到deadlock,我们可以把上面的简化:

func main(){
    channel := make(chan int, 2)

    fmt.Println(1)
    channel <- 1
    fmt.Println(2)
    channel <- 2

    fmt.Println(3)
    channel <- 3
}
func main(){
    channel := make(chan int, 2)

    fmt.Println(1)
    channel <- 1
    <-channel
    <-channel
}   
func main(){
    channel := make(chan int)

    fmt.Println(1)
    channel <- 1
}

总之一句话,在没有其他goroutine在运行时(这是一个大的前提),对于有缓冲的,满了就不要写,还没写进去就不要读。对于没有缓冲的读写都甭想!

解决方案

可以用select来帮忙处理,这样可以丢弃掉3;

func main(){
    channel := make(chan int, 2)

    fmt.Println(1)
    channel <- 1
    fmt.Println(2)
    channel <- 2

    select {
    case channel <- 3:
        fmt.Println(3)
    default:
        fmt.Println("channel full")
    }
}

对于读的死锁也可以同样处理,这里就不多说。

那对于上面的panic那个问题,用select就不好实现,因为channel里的个数是个变量,所以我这边的解决方案是在recover里面写入channel一个值,这样就能保证读的那一方可以读到所有的啦。

其实这都是goroutine的一些基本的知识,看来还是不牢固啊!


转载请注明出处:http://www.opscoder.info/golang_goroutine_deadlock.html

其他分类: