为啥在同一个 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 <-
包裹在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 <- 1
时,它想确保通道中的消息将在某处收到(即 <-c
)。 Go 不知道此时是否会收到频道。所以 go 将等待正在运行的 goroutine 完成,直到发生以下任一情况:
-
所有的 goroutine 都完成了(睡着了)
其中一个 goroutine 尝试接收通道
在情况 #1 中,go 将出错并显示上述消息,因为现在 go 知道 goroutine 无法接收通道并且它需要一个。
在情况 #2 中,程序将继续,因为现在知道该频道已被接收。这解释了OP的例子中的成功案例。
【讨论】:
【参考方案4】: 缓冲消除了同步。 缓冲使它们更像 Erlang 的邮箱。 缓冲通道对于某些问题可能很重要,但它们更难以推理 默认情况下,通道是无缓冲的,这意味着它们只接受发送 (chan 缓冲通道接受有限数量的 这些值没有对应的接收者。messages := make(chan string, 2) //-- 最多缓冲 2 个值的字符串通道。
通道上的基本发送和接收是阻塞的。
但是,我们可以使用select
和default
子句来实现非阻塞 发送、接收,甚至是非阻塞多路select
s。
【讨论】:
以上是关于为啥在同一个 goroutine 中使用无缓冲通道会导致死锁?的主要内容,如果未能解决你的问题,请参考以下文章
Golang:为啥增加缓冲通道的大小会消除我的 goroutine 的输出?
Golang入门到项目实战 golang并发变成之通道channel