尝试两次获得价值时陷入僵局

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

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) // game on; toss the ball
    time.Sleep(1 * time.Second)
    fmt.Println(<-table) // game over; grab the ball
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- 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 (
    "fmt"
    "time"
)

type Ball struct{ hits int }

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

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

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- 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?

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 := <-table, blocked until table is written to
  • Player "pong" created, tries to read from table with ball := <-table, blocked until table is written to
  • Main writes the Ball to table with the following line:

     table <- 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 := <-table)
  • Ping puts the ball on the table with table <- ball, not blocked because pong is waiting
  • Pong reads the ball (execution continues after pong's ball := <-table)
  • Pong puts the ball on the table with table <- ball, not blocked because ping is waiting
  • Ping reads the ball (execution continues after ping's ball := <-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(<-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 := <- table.

If you do a further <-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.