【问题标题】:Go channels and deadlock去通道和死锁
【发布时间】:2011-08-24 10:08:02
【问题描述】:

我正在尝试理解 Go 语言。我试图创建两个 goroutine 使用两个通道链接它们之间的流:

func main() {
c1 := make(chan int)
c2 := make(chan int)

go func() {
    for i := range c1{
        println("G1 got", i)
        c2 <- i
    }
}()

go func() {
    for i := range c2 {
        println("G2 got", i)
        c1 <- i
    }
}()


c1 <- 1

time.Sleep(1000000000 * 50)
}

正如预期的那样,这段代码会打印出来:

 G1 got 1
 G2 got 1
 G1 got 1
 G2 got 1
 ....

直到主函数退出。

但是如果我从主通道向其中一个通道发送另一个值,它会突然阻塞:

func main() {
c1 := make(chan int)
c2 := make(chan int)

go func() {
    for i := range c1{
        println("G1 got", i)
        c2 <- i
    }
}()

go func() {
    for i := range c2 {
        println("G2 got", i)
        c1 <- i
    }
}()


c1 <- 1

time.Sleep(1000000000 * 1)

c1 <- 2

time.Sleep(1000000000 * 50)
}

输出

G1 got 1
G2 got 1
G1 got 1
G2 got 1
G1 got 2

然后阻塞直到主要结束。

发送给 c1 的值“2”到达第一个 goroutie,后者将其发送给 c2,但第二个 goroutine 从不接收。

(在此示例中使用大小为 1(c1 或 c2)的缓冲通道)

为什么会这样?当在真实代码中发生这种情况时,我该如何调试它?

【问题讨论】:

    标签: go channel


    【解决方案1】:

    使用make(chan int) 创建的 Go 频道不会被缓冲。如果您想要一个缓冲通道(不一定会阻塞),请使用make(chan int, 2) 进行设置,其中 2 是通道的大小。

    关于无缓冲通道的事情是它们也是同步的,所以它们总是在和读时阻塞。

    它死锁的原因是你的第一个 goroutine 正在等待它的 c2 &lt;- i 完成,而第二个 goroutine 正在等待 c1 &lt;- i 完成,因为 c1 中有一个额外的东西。当这种事情发生在实际代码中时,我发现调试此类事情的最佳方法是查看哪些 goroutines 被阻塞并认真思考。

    如果确实需要,您也可以通过仅使用同步通道来回避问题。

    【讨论】:

    • 当缓冲区填满时,缓冲通道也会发生同样的事情吗?
    • @ithkuil:在这种情况下,所有通道的所有缓冲区都必须填满,因此您的示例不会导致它。如果您的程序总是投入的渠道多于投入的渠道,那么您最终会遇到问题。
    • 所以你只需要选择一个任意大的缓冲区大小并希望它永远不会填满?似乎不太可靠。
    【解决方案2】:

    nmichaels 的回答是正确的,但我想我要补充一点,在调试此类问题时,有一些方法可以找出您陷入僵局的位置。

    一个简单的方法是,如果您使用的是类 Unix 操作系统,请运行命令

    kill -6 [pid]
    

    这将终止程序并为每个 goroutine 提供堆栈跟踪。

    一个稍微复杂一点的方法是附加 gdb。

    gdb [executable name] [pid]
    

    您可以照常检查活动 goroutine 的堆栈和变量,但据我所知,没有简单的方法可以切换 goroutine。您可以按常规方式切换操作系统线程,但这可能还不够。

    【讨论】:

    • 感谢您的提示!有没有办法在不终止进程的情况下打印堆栈跟踪?也许,从进程内部调用了一些运行时函数?
    • 是的,您可以导入运行时/调试并调用 Stack 获取字符串或调用 PrintStack 仅转储到标准输出。如果您的应用程序有一个单独的 goroutine 处理来自 os/signals 的信号,我想您仍然可以使用它来调试死锁。
    • 感谢kill -6:找到频道被阻止的最简单方法!
    【解决方案3】:

    为了防止通道溢出,可以在再次写入之前询问通道的当前容量并擦干。

    在我的例子中,游戏以 60fps 进行,鼠标移动得更快,所以在再次写入之前检查通道是否已清除总是好的。

    注意之前的数据丢失了

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        // you must specify the size of the channel, 
        // even for just one element, or the code doesn't work
        ch := make( chan int, 1 )
        fmt.Printf("len: %v\n", len(ch))
        fmt.Printf("cap: %v\n\n", cap(ch))
    
        ch <- 1
    
        for i := 0; i != 100; i += 1 {
            fmt.Printf("len: %v\n", len(ch))
            fmt.Printf("cap: %v\n\n", cap(ch))
    
            if cap( ch ) == 1 {
                <- ch
            }
    
            ch <- i
    
            fmt.Printf("len: %v\n", len(ch))
            fmt.Printf("cap: %v\n\n", cap(ch))
        }
        fmt.Printf("end!\n")
    }
    

    【讨论】:

      猜你喜欢
      • 2019-04-05
      • 2017-10-05
      • 1970-01-01
      • 2021-06-16
      • 1970-01-01
      • 1970-01-01
      • 2017-06-24
      • 2018-02-02
      • 2018-09-20
      相关资源
      最近更新 更多