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

huangapple go评论69阅读模式
英文:

Why does the use of an unbuffered channel in the same goroutine result in a deadlock?

问题

我确定对于这个微不足道的情况有一个简单的解释,但是我对于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 <-包装在一个goroutine中使得例子按照我们的预期运行:

package main

import "fmt"

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

再次,为什么?

请给我一个深入的解释,而不仅仅是如何消除死锁和修复代码。

英文:

I'm sure that there is a simple explanation to this trivial situation, but I'm new to the go concurrency model.

when I run this example

package main

import &quot;fmt&quot;

func main() {
	c := make(chan int)    
	c &lt;- 1   
	fmt.Println(&lt;-c)
}

I get this error :

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

Why ?


Wrapping c &lt;- in a goroutine makes the example run as we expected

package main

import &quot;fmt&quot;
    
func main() {
    c := make(chan int)        
    go func(){
       c &lt;- 1
    }()
    fmt.Println(&lt;-c)
}

Again, why ?

Please, I need deep explanation , not just how to eliminate the deadlock and fix the code.

答案1

得分: 108

文档中可以得知:

> 如果通道是无缓冲的,发送者会阻塞直到接收者接收到值。如果通道有缓冲,发送者只会阻塞直到值被复制到缓冲区;如果缓冲区已满,这意味着需要等待直到某个接收者取走一个值。

换句话说:

  • 当通道满时,发送者会等待另一个goroutine通过接收来腾出空间。
  • 你可以将无缓冲通道看作是一个始终满的通道:必须有另一个goroutine来接收发送者发送的内容。

这行代码

c &lt;- 1

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

你可以通过将通道的创建方式更改为

c := make(chan int, 1)

这样在通道阻塞之前就有一个项目的空间。

但这并不是并发的目的。通常情况下,你不会在没有其他goroutine处理其中内容的情况下使用通道。你可以像这样定义一个接收的goroutine:

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

演示

英文:

From the documentation :

> If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value
> has been copied to the buffer; if the buffer is full, this means
> waiting until some receiver has retrieved a value.

Said otherwise :

  • when a channel is full, the sender waits for another goroutine to make some room by receiving
  • you can see an unbuffered channel as an always full one : there must be another goroutine to take what the sender sends.

This line

c &lt;- 1

blocks because the channel is unbuffered. As there's no other goroutine to receive the value, the situation can't resolve, this is a deadlock.

You can make it not blocking by changing the channel creation to

c := make(chan int, 1) 

so that there's room for one item in the channel before it blocks.

But that's not what concurrency is about. Normally, you wouldn't use a channel without other goroutines to handle what you put inside. You could define a receiving goroutine like this :

func main() {
    c := make(chan int)    
    go func() {
        fmt.Println(&quot;received:&quot;, &lt;-c)
    }()
    c &lt;- 1   
}

Demonstration

答案2

得分: 18

在非缓冲通道中,只有在有接收者等待接收数据时,写入通道的操作才会发生。这意味着在下面的示例中:

func main(){
    ch := make(chan int)
    ch <- 10   /* 主例程被阻塞,因为没有例程来接收这个值 */
    <- ch
}

现在,在有其他Go例程的情况下,相同的原则适用:

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

这将起作用,因为task例程在写入非缓冲通道之前等待数据被消费。

为了更清楚地说明,让我们交换主函数中第二个和第三个语句的顺序:

func main(){
    ch := make(chan int)
    ch <- 10       /* 被阻塞:没有例程等待从通道中消费数据 */
    go task(ch)
}

这将导致死锁。

所以简而言之,只有在有某个例程等待从通道中读取数据时,才会将数据写入非缓冲通道,否则写入操作将永远被阻塞,并导致死锁。

注意:相同的概念也适用于缓冲通道,但发送者不会被阻塞,直到缓冲区满了,这意味着接收者不一定与每个写入操作同步。

因此,如果我们有大小为1的缓冲通道,那么你上面提到的代码将起作用:

func main(){
    ch := make(chan int, 1) /* 大小为1的通道 */
    ch <- 10  /* 不被阻塞:可以将值放入通道缓冲区 */
    <- ch 
}

但是,如果我们向上面的示例写入更多的值,那么将会发生死锁:

func main(){
    ch := make(chan int, 1) /* 缓冲区大小为1的通道 */
    ch <- 10
    ch <- 20 /* 被阻塞:因为缓冲区大小已满,没有人等待从通道中接收数据 */
    <- ch
    <- ch
}
英文:

In unbuffered channel writing to channel will not happen until there must be some receiver which is waiting to receive the data, which means in the below example

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

Now In case where we have other go routine, the same principle applies

func main(){
  ch :=make(chan int)
  go task(ch)
  ch &lt;-10
}
func task(ch chan int){
   &lt;- ch
}

This will work because task routine is waiting for the data to be consumed before writes happen to unbuffered channel.

To make it more clear, lets swap the order of second and third statements in main function.

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

This will leads to Deadlock

So in short, writes to unbuffered channel happens only when there is some routine waiting to read from channel, else the write operation is blocked forever and leads to deadlock.

NOTE: The same concept applies to buffered channel, but Sender is not blocked until the buffer is full, which means receiver is not necessarily to be synchronized with every write operation.

So if we have buffered channel of size 1, then your above mentioned code will work

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

But if we write more values to above example, then deadlock will happen

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

答案3

得分: 5

在这个答案中,我将尝试通过解释错误信息来揭示一些关于Go语言中通道和goroutine工作原理的细节。

第一个例子是:

package main

import "fmt"

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

错误信息是:

fatal error: all goroutines are asleep - deadlock!

在代码中,根本没有使用goroutine(顺便说一句,这个错误是在运行时而不是编译时发生的)。当Go运行到c <- 1这一行时,它想要确保通道中的消息会被接收(即<-c)。此时,Go并不知道通道是否会被接收。因此,Go会等待正在运行的goroutine完成,直到发生以下情况之一:

  1. 所有的goroutine都已经完成(休眠)。
  2. 其中一个goroutine尝试接收通道。

在情况#1中,Go会报错并显示上述消息,因为现在Go知道没有任何一个goroutine会接收这个通道,而它需要一个接收者。

在情况#2中,程序将继续执行,因为现在Go知道这个通道已经被接收。这解释了OP示例中的成功情况。

英文:

In this answer, I will try to explain the error message through which we can peek a little bit into how go works in terms of channels and goroutines

The first example is:

package main

import &quot;fmt&quot;

func main() {
	c := make(chan int)    
	c &lt;- 1   
	fmt.Println(&lt;-c)
}

The error message is:

fatal error: all goroutines are asleep - deadlock!

In the code, there are NO goroutines at all (BTW this error is in runtime, not compile time). When go runs this line c &lt;- 1, it wants to make sure that the message in the channel will be received somewhere (i.e &lt;-c). Go does NOT know if the channel will be received or not at this point. So go will wait for the running goroutines to finish until either one of the following happens:

  1. all of the goroutines are finished(asleep)
  2. one of the goroutine tries to receive the channel

In case #1, go will error out with the message above, since now go KNOWS that there is no way that a goroutine will receive the channel and it need one.

In case #2, the program will continue, since now go KNOWS that this channel is received. This explain the successful case in OP's example.

答案4

得分: -2

  • 缓冲可以去除同步。
  • 缓冲使它们更像是 Erlang 的邮箱。
  • 对于某些问题来说,缓冲通道可能很重要,但对其进行推理更加微妙。
  • 默认情况下,通道是无缓冲的,这意味着它们只会接受发送(chan <-),如果有相应的接收(<- chan)准备好接收发送的值。
  • 缓冲通道可以接受有限数量的值,而无需相应的接收方接收这些值。

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

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

英文:
  • Buffering removes synchronization.
  • Buffering makes them more like Erlang's mailboxes.
  • Buffered channels can be important for some problems but they are more subtle to reason about
  • By default channels are unbuffered, meaning that they will only accept sends
    (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value.
  • Buffered channels accept a limited number of
    values without a corresponding receiver for those values.

messages := make(chan string, 2) //-- channel of strings buffering up to 2 values.

Basic sends and receives on channels are blocking.
However, we can use select with a default clause to implement non-blocking sends, receives, and even non-blocking multi-way selects.

huangapple
  • 本文由 发表于 2013年9月6日 22:47:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/18660533.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定