Go语言死锁解析

发表时间: 2024-04-02 09:55

fatal error: all goroutines are asleep - deadlock

近两天遇到此类错误, 发现goroutine以及channel的基础仍需巩固。由该错误牵引出go相关并发操作的问题, 下面做一些简单的tips操作和记录。

package mainimport (    "fmt")func hello() {    fmt.Println("Hello Goroutine!")}func main() {    go hello() // 启动另外一个goroutine去执行hello函数    fmt.Println("main goroutine done!")}

1、在程序启动时, Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了, 所有在main()函数中启动的goroutine会一同结束!

所以引出sync.WaitGroup的使用。通过它, 可以实现goroutine的同步。

package mainimport ("fmt""sync")var wg sync.WaitGroupfunc hello(i int) {    defer wg.Done() // goroutine结束就登记-1    fmt.Println("Hello Goroutine!", i)}func main() {    for i := 0; i < 10; i++ {        wg.Add(1) // 启动一个goroutine就登记+1        go hello(i)    }    wg.Wait() // 等待所有登记的goroutine都结束}

2、单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。如果说goroutine是Go程序并发的执行体, channel就是它们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。Go 语言中的通道(channel)是一种特殊的类型。

通道像一个传送带或者队列, 总是遵循先入先出(First In First Out)的规则, 保证收发数据的顺序。每一个通道都是一个具体类型的导管, 也就是声明channel的时候需要为其指定元素类型。

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。我们通过调用内置的close函数来关闭通道。

关闭后的通道有以下特点:

(1) 对一个关闭的通道再发送值就会导致panic。

(2) 对一个关闭的通道进行接收会一直获取值直到通道为空。

(3) 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。

(4) 关闭一个已经关闭的通道会导致panic。

无缓冲的通道又称为阻塞的通道:

package mainimport (    "fmt")func main() {    ch := make(chan int)    ch <- 10    fmt.Println("发送成功")}

上面这段代码能够通过编译, 但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!


goroutine 1 [chan send]:

main.main()

我们使用ch := make(chan int)创建的是无缓冲的通道, 无缓冲的通道只有在有人接收值的时候才能发送值。

上面的代码会阻塞在ch <- 10这一行代码形成死锁, 那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值, 并一种方式是使用带缓冲的通道, 例如:

package mainimport (    "fmt")func main() {    ch := make(chan int, 1)    ch <- 1 // 发送通道    // 接收通道    fmt.Println(<-ch) // 1}
package mainimport (    "fmt")func recv(c chan int) {    ret := <-c    fmt.Println("接收成功", ret)}func main() {    ch := make(chan int)    go recv(ch) // 启用goroutine从通道接收值    ch <- 10    fmt.Println("发送成功")}

但是注意:channel 通道增加缓存区后, 可将数据暂存到缓冲区, 而不需要接收端同时接收(缓冲区如果超出大小同样会造成死锁)

package mainimport (    "fmt")func main() {    ch := make(chan int, 1) // 定义缓冲区的大小为 1    ch <- 1 // 发送通道    ch <- 1 // 发送通道    // 接收通道    fmt.Println(<-ch) // 1    /*    fatal error: all goroutines are asleep - deadlock!    goroutine 1 [chan send]:    */}

解决方法: 重新定义缓冲区大小, ch := make(chan int, 2)


channel异常情况总结

channel nil 非空 空的 满了 没满

接收 阻塞 接收值 阻塞 接收值 接收值

发送 阻塞 发送值 发送值 阻塞 发送值

关闭 panic 关闭成功 关闭成功 关闭成功 关闭成功,

读完数据后返回零值 返回零值 读完数据后返回零值 读完数据后返回零值

总结, 可以看出, 产生阻塞的方式, 主要容易踩坑的有两种:空的通道一直接收会阻塞; 满的通道一直发送也会阻塞!

3、那么, 如何解决阻塞死锁问题呢?

(1) 如果是上面的无缓冲通道, 使用再起一个协程的方式, 可使得接收端和发送端并行执行。

(2) 可以初始化时就给channel增加缓冲区, 也就是使用有缓冲的通道

(3) 易踩坑点, 针对有缓冲的通道, 产生阻塞, 如何解决?

如下面例子, 开启多个goroutine并发执行任务, 并将数据存入管道channel, 后续读取数据:

package mainimport (    "fmt"    "time")func request(index int, ch chan<- string) {    time.Sleep(time.Duration(index) * time.Second)    s := fmt.Sprintf("编号%d完成", index)    ch <- s}func main() {    ch := make(chan string, 10)    fmt.Println(ch, len(ch))    for i := 0; i < 4; i++ {        go request(i, ch)}for ret := range ch {    fmt.Println(len(ch))    fmt.Println(ret)    }}

错误如下:

0xc000056060 0

0

编号0完成

0

编号1完成

0

编号2完成

0

编号3完成

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:

main.main()

F:/project/test/suoding/deadlock/demo7.go:22 +0x1ad

exit status 2


不可靠的解决方式如下:

for {    i, ok := <-ch // 通道关闭后再取值ok=false;通道为空去接收,会发生阻塞死锁    if !ok {        break    }    println(i)}for ret := range ch {    fmt.Println(len(ch))    fmt.Println(ret)}

以上两种从通道获取方式, 都有小坑! 一旦获取的通道没有主动close(ch)关闭, 而且通道为空时, 无论通过for还是foreach方式去取值获取, 都会产生阻塞死锁deadlock chan receive错误!

可靠的解决方式1 如下:

package mainimport ("fmt""sync""time")var wg sync.WaitGroupfunc request(index int, ch chan<- string) {    time.Sleep(time.Duration(index) * time.Second)    s := fmt.Sprintf("编号%d完成", index)    ch <- s    defer wg.Done()}func main() {    ch := make(chan string, 10)    go func() {        wg.Wait()        close(ch)    }()    for i := 0; i < 4; i++ {        wg.Add(1)        go request(i, ch)    }    for ret := range ch {        fmt.Println(len(ch))        fmt.Println(ret)    }}

解决方式: 即我们在生成完4个goroutine后对data channel进行关闭, 这样通过for range从通道循环取出全部值, 通道关闭就会退出for range循环。

具体实现:可以利用sync.WaitGroup解决, 在所有的 data channel 的输入处理之前, wg.Wait()这个goroutine会处于等待状态(wg.Wait()源码就是for循环)。

当执行方法处理完后(wg.Done), wg.Wait()就会放开执行, 执行后面的close(ch)。

可靠的解决方式2 如下:

package mainimport ("fmt""time")func request(index int, ch chan<- string) {    time.Sleep(time.Duration(index) * time.Second)    s := fmt.Sprintf("编号%d完成", index)    ch <- s}func main() {    ch := make(chan string, 10)    for i := 0; i < 4; i++ {    go request(i, ch)}for {    select {    case i := <-ch: // select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句    println(i)    default:    time.Sleep(time.Second)    fmt.Println("无数据")    }    }}

上面这种方式获取, 通过select case + default的方式也可以完美避免阻塞死锁报错! 但是适用于通道不关闭, 需要时刻循环执行数据并且处理的情境下。

一定留意, default的作用很大! 是避免阻塞的核心。

使用select语句能提高代码的可读性。

可处理一个或多个channel的发送/接收操作。

如果多个case同时满足, select会随机选择一个。

对于没有case的select{}会一直等待, 可用于阻塞main函数。

5、实际项目中goroutine+channel+select的使用

如下, 使用于项目监听终端中断信号操作:


package mainimport ("fmt""os""os/signal""syscall")func main() {    go SignalProc()    done := make(chan bool, 1)    for {    select {        case <-done:        break        }    }    fmt.Println("exit")}func SignalProc() {    sigs := make(chan os.Signal)    signal.Notify(sigs, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGHUP, os.Interrupt)    for {    msg := <-sigs    fmt.Println("Recevied signal:", msg)    switch msg {    default:    fmt.Println("get sig=%v\n", msg)    case syscall.SIGHUP:    fmt.Println("get sighup\n")    case syscall.SIGUSR1:    fmt.Println("SIGUSR1 test")    case syscall.SIGUSR2:    fmt.Println("SIGUSR2 test")    }    }}


// kill -USR1 10323
kill -USR2 10323
kill -n 2 10323
可以 SIGUSR1 做一些配置的重新加载
SIGUSR2 可以做一些游戏base的重新加载