当尝试两次接收值时出现死锁。

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

Get deadlock when try to received value twice

问题

我看了关于高级Go并发模式的精彩视频。在开头,Sameer Ajmani展示了一个乒乓球应用程序。

package main

import (
	"fmt"
	"time"
)

type Ball struct{ hits int }

func main() {
	table := make(chan *Ball)
	go player("ping", table)
	go player("pong", table)

	table <- new(Ball) // 游戏开始;抛出球
	time.Sleep(1 * time.Second)
	fmt.Println(<-table) // 游戏结束;抓住球
}

func player(name string, table chan *Ball) {
	for {
		ball := <-table
		ball.hits++
		fmt.Println(name, ball.hits)
		time.Sleep(100 * time.Millisecond)
		table <- ball
	}
}

我理解这段代码的工作原理,大约90%。有两个goroutine,它们在主线程休眠期间互相发送消息,ping和pong。

然后我尝试了以下代码:

package main

import (
	"fmt"
	"time"
)

type Ball struct{ hits int }

func main() {
	table := make(chan *Ball)
	go player("ping", table)
	go player("pong", table)

	table <- new(Ball) // 游戏开始;抛出球
	time.Sleep(1 * time.Second)
	fmt.Println(<-table) // 游戏结束;抓住球
	fmt.Println(<-table) // 游戏结束;抓住球
}

func player(name string, table chan *Ball) {
	for {
		ball := <-table
		ball.hits++
		fmt.Println(name, ball.hits)
		time.Sleep(100 * time.Millisecond)
		table <- ball
	}
}

我在这里遇到了一个死锁,真的不明白为什么。看一下goroutine中的最后一行,在后台,两个goroutine仍然继续循环并相互发送值。对我来说,似乎是对table变量通道的多个接收者。

我的主要问题是,为什么第二个示例中会发生死锁?

英文:

I watch the awesome video about Advanced Go Concurrency Patterns. At the beginning Sameer Ajmani shows a ping pong application.

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

type Ball struct{ hits int }

func main() {
	table := make(chan *Ball)
	go player(&quot;ping&quot;, table)
	go player(&quot;pong&quot;, table)

	table &lt;- new(Ball) // game on; toss the ball
	time.Sleep(1 * time.Second)
	fmt.Println(&lt;-table) // game over; grab the ball
}

func player(name string, table chan *Ball) {
	for {
		ball := &lt;-table
		ball.hits++
		fmt.Println(name, ball.hits)
		time.Sleep(100 * time.Millisecond)
		table &lt;- ball
	}
}

The code how it works, I understand to 90 percent. They are two goroutines they send to each other messages, ping and pong, during the main thread sleeps.

Then I try following

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

type Ball struct{ hits int }

func main() {
	table := make(chan *Ball)
	go player(&quot;ping&quot;, table)
	go player(&quot;pong&quot;, table)

	table &lt;- new(Ball) // game on; toss the ball
	time.Sleep(1 * time.Second)
	fmt.Println(&lt;-table) // game over; grab the ball
	fmt.Println(&lt;-table) // game over; grab the ball
}

func player(name string, table chan *Ball) {
	for {
		ball := &lt;-table
		ball.hits++
		fmt.Println(name, ball.hits)
		time.Sleep(100 * time.Millisecond)
		table &lt;- ball
	}
}

I've got here a deadlock and really do not understand why. Look at the last line in the go routine, I try to receive value from channel like the second last line. In background the two goroutines still continue loop and send to each other value. It seems to be for me a multiple receiver for table variable channel.

My main question is, what I've got by the second sample a deadlock?

答案1

得分: 3

在后台,两个goroutine仍然继续循环并相互发送值。

不,它们不会这样做。

当你使用make(chan *Ball)创建通道时,你创建了一个无缓冲通道。这相当于说make(chan *Ball, 0),这意味着通道中可以容纳0个元素 - 或者更明确地说,对通道的写入将被阻塞,直到另一个例程从通道中读取,反之亦然。

无缓冲通道的执行顺序如下:

  • 创建"ping"选手,尝试从table中读取ball := <-table,被阻塞,直到table被写入

  • 创建"pong"选手,尝试从table中读取ball := <-table,被阻塞,直到table被写入

  • 主线程使用以下代码将球写入table

       table <- new(Ball) // 游戏开始;传球
    

    这不会被阻塞,因为有人在等待从通道中读取。

  • 现在"ping"读取球(在"ping"的ball := <-table之后继续执行)

  • "ping"将球放在桌子上table <- ball,不会被阻塞,因为"pong"在等待

  • "pong"读取球(在"pong"的ball := <-table之后继续执行)

  • "pong"将球放在桌子上table <- ball,不会被阻塞,因为"ping"在等待

  • "ping"读取球(在"ping"的ball := <-table之后继续执行)

  • ....等等
    直到主线程通过读取球而不是读取其中一个选手来结束游戏。

这是使用通道确保只有一个例程在运行的一个很好的例子。

为了结束游戏,主线程在一秒钟后从通道中取走球:

 time.Sleep(1 * time.Second)
 fmt.Println(<-table) // 游戏结束;抓住球

此后,table通道将为空,任何进一步对它的读取都将被阻塞。

  • 此时,两个player例程都在ball := <-table处被阻塞。

如果在主线程中进行进一步的<-table读取,那个读取也将被阻塞,直到有例程尝试向table通道写入。然而,由于没有其他例程在运行,你将会遇到死锁(所有goroutine都被阻塞)。


好的。那我能把两个球放在通道里吗?

不行。

如果我们尝试将两个球放入通道,我们可能会得到以下输出:

Ping 1
Pong 1

因为两个player例程都将被卡在尝试将球放回通道的位置上 - 但没有人尝试读取它。执行顺序如下:

  • 创建"ping"选手,尝试从table中读取,被阻塞
  • 创建"pong"选手,尝试从table中读取,被阻塞
  • 主线程将第一个球放入table(不会被阻塞,因为有人在等待读取)
  • 主线程将第二个球放入table(不会被阻塞,因为有人在等待读取)
  • "ping"读取第一个球
  • "pong"读取第二个球
  • "ping"将第一个球放在桌子上,被阻塞,因为没有人在等待读取
  • "pong"将第二个球放在桌子上,被阻塞,因为没有人在等待读取
  • 两个选手都被阻塞
    直到主线程通过读取两个球来结束游戏。

这是一个示例程序

正如一位评论者指出的,结束游戏的更好方法是关闭通道。但是,我希望这次讨论能够解决你的困惑。

英文:

> In background the two goroutines still continue loop and send to each other value.

No, they don't.

When you make the channel with make(chan *Ball), you are making an unbuffered channel. This is equivalent to saying make(chan *Ball,0), which means that the channel can fit 0 things in it - or, more explicitly, any writes to the channel will block until another routine reads from the channel, and vice versa.

The order of execution with an unbuffered channel is this:

  • Player "ping" created, tries to read from table with ball := &lt;-table, blocked until table is written to

  • Player "pong" created, tries to read from table with ball := &lt;-table, blocked until table is written to

  • Main writes the Ball to table with the following line:

       table &lt;- new(Ball) // game on; toss the ball
    

This is not blocked because someone is waiting to read on the channel.

  • Now ping reads the ball (execution continues after ping's ball := &lt;-table)
  • Ping puts the ball on the table with table &lt;- ball, not blocked because pong is waiting
  • Pong reads the ball (execution continues after pong's ball := &lt;-table)
  • Pong puts the ball on the table with table &lt;- ball, not blocked because ping is waiting
  • Ping reads the ball (execution continues after ping's ball := &lt;-table)
  • ....etc
    • Until main ends the game by reading the ball instead of one of the players

This is a great example of using a channel to ensure that only one routine is running at once.

To end the game, the main thread simply snatches the ball out of the channel after one second:

 time.Sleep(1 * time.Second)
 fmt.Println(&lt;-table) // game over; grab the ball

After this, the table channel will be empty, and any further reads on it will block.

  • At this point, both player routines are blocked at ball := &lt;- table.

If you do a further &lt;-table read in the main thread, that read will also block, until a routine tries to write to the table channel. However, since no other routines are running, you have a deadlock (all goroutines are blocked).


Ok. Can I just put two balls in the channel then?

No.

If we try to put two balls in the channel, we'll probably get the output:

Ping 1
Pong 1

because both player routines will be stuck trying to put their ball back into the channel - but nobody will be trying to read it. The order looks like this:

  • Player "ping" created, tries to read from table, blocked
  • Player "pong" created, tries to read from table, blocked
  • main puts the first ball into table (not blocked because someone is waiting to read)
  • main puts the second ball into table (not blocked because someone is waiting to read)
  • Ping reads the first ball
  • Pong reads the second ball
  • Ping puts the first ball on the table, blocked because nobody is waiting to read
  • Pong puts the second ball on the table, blocked because nobody is waiting to read
  • Both players are blocked
    • Until main ends the game by reading both balls.

Here's a program to illustrate

As a commenter points out, a better thing to do to end the game would be to close the channel. But, I hope this discussion has cleared up your confusion.

huangapple
  • 本文由 发表于 2014年9月24日 13:52:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/26009501.html
匿名

发表评论

匿名网友

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

确定