为啥在同一个 goroutine 中使用无缓冲通道会导致死锁?

Posted

技术标签:

【中文标题】为啥在同一个 goroutine 中使用无缓冲通道会导致死锁?【英文标题】:Why does the use of an unbuffered channel in the same goroutine result in a deadlock?为什么在同一个 goroutine 中使用无缓冲通道会导致死锁? 【发布时间】:2013-09-10 17:12:06 【问题描述】:

我确信对于这种微不足道的情况有一个简单的解释,但我是 go 并发模型的新手。

当我运行这个例子时

package main

import "fmt"

func main() 
    c := make(chan int)    
    c <- 1   
    fmt.Println(<-c)

我收到此错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /home/tarrsalah/src/go/src/github.com/tarrsalah/tour.golang.org/65.go:8 +0x52
exit status 2

为什么?


c &lt;- 包裹在goroutine 中会使示例按预期运行

package main

import "fmt"

func main() 
    c := make(chan int)        
    go func()
       c <- 1
    ()
    fmt.Println(<-c)

再说一遍,为什么?

拜托,我需要深入的解释,而不仅仅是如何消除死锁和修复代码。

【问题讨论】:

当您从同一个电话号码拨打您的电话号码时会发生什么?死锁或忙音等。这里是一样的。 【参考方案1】:

来自the documentation:

如果通道没有缓冲,发送方会阻塞,直到接收方收到该值。如果通道有缓冲区,则发送方仅阻塞直到值 已复制到缓冲区;如果缓冲区已满,这意味着 等到某个接收者检索到一个值。

另外说:

当通道已满时,发送方等待另一个 goroutine 通过接收来腾出空间 你可以看到一个无缓冲的通道总是满的:必须有另一个 goroutine 来接收发送者发送的内容。

这一行

c <- 1

阻塞,因为通道没有缓冲。由于没有其他 goroutine 接收该值,情况无法解决,这是一个死锁。

您可以通过将频道创建更改为使其不阻塞

c := make(chan int, 1) 

以便在通道阻塞之前为通道中的一个项目留出空间。

但这不是并发的意义所在。通常,您不会使用没有其他 goroutine 的通道来处理您放入的内容。你可以像这样定义一个接收 goroutine:

func main() 
    c := make(chan int)    
    go func() 
        fmt.Println("received:", <-c)
    ()
    c <- 1   

Demonstration

【讨论】:

我觉得很难去想,有三种情况:1) 没有新 goroutine 的无缓冲通道 -> 死锁,2) 没有新 goroutine 的缓冲通道 -> 没有死锁 3) 无缓冲通道有一个新的 goroutine -> 运行。 当通道已满时,必须有一个 goroutine 来接收 sender 发送的内容,否则 sender 会阻塞,直到有人来。您可以将无缓冲通道视为始终满的通道。 试着把通道想象成一个物理管道:如果它太短,或者是满的,你必须等待另一端有人拿东西,或者把东西放进去会使一些内容落在地面。 对 goroutines 的另一个有用的类比是将它们与硬件电路中的逻辑门进行比较,通道就是电线。在问题的示例中,“门”以一种可以发送或接收的方式错误地连接到自身,但不能同时发送或接收。 不是无缓冲通道总是满的。相反,它们根本不完整。通道就像 goroutine 之间的电缆。他们没有缓冲区。只有通道的输入和输出点可能有称为缓冲区的桶。【参考方案2】:

在无缓冲通道中写入通道不会发生,直到必须有某个接收器正在等待接收数据,这意味着在下面的示例中

func main()
    ch := make(chan int)
    ch <- 10   /* Main routine is Blocked, because there is no routine to receive the value   */
    <- ch

现在如果我们有其他 goroutine,同样的原则也适用

func main()
  ch :=make(chan int)
  go task(ch)
  ch <-10

func task(ch chan int)
   <- ch

这会起作用,因为 task 例程正在等待数据被消耗,然后写入发生在无缓冲通道上。

为了更清楚,让我们交换 main 函数中第二和第三条语句的顺序。

func main()
  ch := make(chan int)
  ch <- 10       /*Blocked: No routine is waiting for the data to be consumed from the channel */
  go task(ch)

这会导致死锁

所以简而言之,只有当有一些例程等待从通道读取时才会写入到无缓冲通道,否则写入操作将永远阻塞并导致死锁。

注意:同样的概念也适用于缓冲通道,但发送方在缓冲区满之前不会被阻塞,这意味着接收方不必与每个写入操作同步。

所以如果我们有大小为 1 的缓冲通道,那么您上面提到的代码就可以工作

func main()
  ch := make(chan int, 1) /*channel of size 1 */
  ch <-10  /* Not blocked: can put the value in channel buffer */
  <- ch 

但是如果我们在上面的例子中写入更多的值,就会发生死锁

func main()
  ch := make(chan int, 1) /*channel Buffer size 1 */
  ch <- 10
  ch <- 20 /*Blocked: Because Buffer size is already full and no one is waiting to recieve the Data  from channel */
  <- ch
  <- ch

【讨论】:

这不是真的“在无缓冲通道中,只有在必须有某个接收器等待接收数据时才会写入通道”。写入通道会发生,当接收器出现时,它将接收数据,如果没有,则会出现死锁或泄漏等。 @InancGumus,在第三个代码中,接收器上面的 sn-p 稍后出现但死锁:play.golang.org/p/HCP5KJ2aW_-【参考方案3】:

在这个答案中,我将尝试解释错误消息,通过它我们可以稍微了解一下 go 在通道和 goroutines 方面是如何工作的

第一个例子是:

package main

import "fmt"

func main() 
    c := make(chan int)    
    c <- 1   
    fmt.Println(<-c)

错误信息是:

fatal error: all goroutines are asleep - deadlock!

在代码中,根本没有 goroutines(顺便说一句,这个错误是在运行时,而不是在编译时)。当 go 运行这行 c &lt;- 1 时,它想确保通道中的消息将在某处收到(即 &lt;-c)。 Go 不知道此时是否会收到频道。所以 go 将等待正在运行的 goroutine 完成,直到发生以下任一情况:

    所有的 goroutine 都完成了(睡着了) 其中一个 goroutine 尝试接收通道

在情况 #1 中,go 将出错并显示上述消息,因为现在 go 知道 goroutine 无法接收通道并且它需要一个。

在情况 #2 中,程序将继续,因为现在知道该频道已被接收。这解释了OP的例子中的成功案例。

【讨论】:

【参考方案4】: 缓冲消除了同步。 缓冲使它们更像 Erlang 的邮箱。 缓冲通道对于某些问题可能很重要,但它们更难以推理 默认情况下,通道是无缓冲的,这意味着它们只接受发送 (chan 缓冲通道接受有限数量的 这些值没有对应的接收者。

messages := make(chan string, 2) //-- 最多缓冲 2 个值的字符串通道。

通道上的基本发送和接收是阻塞的。 但是,我们可以使用selectdefault 子句来实现非阻塞 发送、接收,甚至是非阻塞多路selects。

【讨论】:

以上是关于为啥在同一个 goroutine 中使用无缓冲通道会导致死锁?的主要内容,如果未能解决你的问题,请参考以下文章

Golang:为啥增加缓冲通道的大小会消除我的 goroutine 的输出?

Golang入门到项目实战 golang并发变成之通道channel

为啥数据被推入通道但从未从接收器 goroutine 中读取?

使用长度为零的缓冲通道

Go笔记(十四):通道 channel

单个通道上的多个接收器。谁得到数据?