为什么Go语言中通道接收的顺序会导致死锁的产生/解决?

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

Why is the order of channels receiving causing/resolving a deadlock in Golang?

问题

我已经将我的问题简化为下面的简单示例。我调用了一个带有两个通道的goroutine,并向每个通道发送一条消息。然后,我尝试在稍后接收这些消息。然而,通道接收的顺序很重要。如果我使用发送消息的相同顺序,程序会运行。如果我交换它们,程序就会出错。

我本来期望goroutine能够独立于接收消息而运行,这样我就可以先从任何一个通道接收。但是,通过将消息发送到每个goroutine的单个通道(2个goroutine),我可以解决这个问题。

有人能解释一下为什么这里存在顺序依赖性,以及为什么使用两个单独的goroutine可以解决这种依赖性吗?

package main

import "fmt"

func main() {
	chanA := make(chan string)
	chanB := make(chan string)

	go func() {
		chanA <- "el"
		chanB <- "el"
	}()

	// 如果在A之前接收到B,会导致致命错误
	// 如果在B之前接收到A,会完成
	<-chanB
	<-chanA

	fmt.Println("完成")
}
英文:

I've boiled my issue down to this simple example below. I am invoking a goroutine that takes two channels and sends one message to each. Then I am attempting to receive those messages further along. However, the order of channels receiving matters. If I use the same order I sent the messages, the program runs. If I switch, it does not.

I would have expected the goroutine to run independently from retrieving the messages, allowing me to receive from whichever channel I wanted to first.

I can solve this by sending messages to a single channel per goroutine (2 goroutines).

Could someone explain why there is an order dependence here and why 2 separate goroutines resolves that dependence?

package main

import &quot;fmt&quot;

func main() {
	chanA := make(chan string)
	chanB := make(chan string)

	go func() {
		chanA &lt;- &quot;el&quot;
		chanB &lt;- &quot;el&quot;
	}()

	// if B is received before A, fatal error
	// if A is received before B, completes
	&lt;-chanB
	&lt;-chanA

	fmt.Println(&quot;complete&quot;)
}

答案1

得分: 3

你需要对通道进行缓冲。缓冲通道可以在阻塞之前存储多个元素。

chanA := make(chan string, 1)
chanA <- "el" // 这不会阻塞
fmt.Println("Hello World")

当在上述缓冲通道上执行chanA <- "el"时,元素将被放入缓冲区,线程不会阻塞。如果添加第二个元素,由于缓冲区已满,它将阻塞:

chanA := make(chan string, 1)
chanA <- "el"
chanA <- "el" // <- 这将阻塞,因为缓冲区已满

在你的示例中,缓冲区大小为0。因此,对通道的第一次写入被阻塞,并需要另一个线程读取该值以解除阻塞。

https://go.dev/play/p/6GbsVW4d0Mg

	chanA := make(chan string)
	go func() {
		time.Sleep(time.Second)
		fmt.Println("Pop:", <-chanA) // 解除写入阻塞
	}()
	chanA <- "el"

额外知识

如果你不希望线程阻塞,可以将通道插入操作包装在一个select语句中。这将确保如果通道已满,你的应用程序不会发生死锁。修复这个问题的一种简单方法是增加缓冲区的大小...

https://go.dev/play/p/kKR-lrCO4FX

	select {
	case chanA <- "el":
	default:
		return fmt.Errorf("value not written: %s", value)
	}
英文:

You will need to buffer your channels. A buffered channel can store so many elements before it will block.

chanA := make(chan string, 1)
chanA &lt;- &quot;el&quot; // This will not block
fmt.Println(&quot;Hello World&quot;)

When you do chanA &lt;- &quot;el&quot; on the buffered channel above, the element gets placed into the buffer and the thread does not block. If you add a second element, it will then block as there is no room in the buffer:

chanA := make(chan string, 1)
chanA &lt;- &quot;el&quot;
chanA &lt;- &quot;el&quot; // &lt;- This will block, as the buffer is full

In your example, you have a buffer of 0. So the first write to the channel is blocked, and requires another thread to read the value to unblock.

https://go.dev/play/p/6GbsVW4d0Mg

	chanA := make(chan string)
	go func() {
		time.Sleep(time.Second)
		fmt.Println(&quot;Pop:&quot;, &lt;-chanA) // Unblock the writer
	}()
	chanA &lt;- &quot;el&quot;

Extra knowledge

If you do not want a thread to block, you can wrap a channel insert in a select. This will ensure if the channel is full, your application does not deadlock. One cheap way of fixing this is a larger buffer...

https://go.dev/play/p/kKR-lrCO4FX

	select {
	case chanA &lt;- &quot;el&quot;:
	default:
		return fmt.Errorf(&quot;value not written: %s&quot;, value)
	}

答案2

得分: 1

这是goroutine的工作原理:

一个goroutine在读/写通道时会被阻塞,直到找到另一个从相同通道读/写的goroutine。

请注意上述引用中的读/写写/读

在你的情况下,你使用go启动的匿名goroutine会等待在channelA上写入,直到找到一个从channelA读取的goroutine。
主goroutine会等待从channelB读取,除非找到一个从它读取的goroutine。

你可以这样理解,任何在读/写通道之后写入的代码行都不会被考虑,除非go找到另一个从相同通道读/写的goroutine。

所以,如果你改变读或写的顺序,就不会发生死锁,或者说另一个goroutine也可以完成任务。

希望清楚明白。

英文:

this is how goroutine works:

> a goroutine will be blocked read/write on a channel unless if find another goroutine which write/read from the same channel.

Pay attention to read/write and write/read in the above blocked quote.

In your case, your anon goroutine(which you kicked off with go) waits to write on channelA until it finds a goroutine which reads from channelA.
The main goroutine waits to read from channelB unless it finds a goroutine that reads from it.

You can think it this way, any line written after read/write to channel won't be considered unless go finds another routine which write/read from the same channel.

So, if you change either read or write order you will not have deadlock or as you said another goroutine will do the job too.

Hope it's clear.

答案3

得分: 0

对于无缓冲通道的写入或读取操作,会阻塞直到有一个 goroutine 可以进行写入或读取操作。当 goroutine 向 a 写入时,它会阻塞直到主 goroutine 可以从 a 读取,但是主 goroutine 也被阻塞在等待从 b 读取的操作上,因此发生了死锁。

英文:

A write to or read from an unbuffered channel will block until there is a goroutine to write to or read from that channel. When the goroutine writes to a, it will block until the main goroutine can read from a, but main goroutine is also blocked waiting to read from b, hence deadlock.

huangapple
  • 本文由 发表于 2022年2月23日 05:13:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/71228367.html
匿名

发表评论

匿名网友

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

确定