Go

Implement ping-pong between two goroutines that stops cleanly after N rounds or on context cancellation.

Question 517MediumGo 1.22 to 1.25

Pass a "ball" (a hit counter) between two players. Each player selects on both its input channel and ctx.Done(). The same applies to the send: a send that is not inside a select can block forever if the other player has already quit. The player that sees the last round calls cancel(), so its partner wakes up and exits too.

func PingPong(ctx context.Context, rounds int) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	ping, pong := make(chan int), make(chan int)
	var wg sync.WaitGroup

	player := func(name string, in <-chan int, out chan<- int) {
		defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				return
			case ball := <-in:
				if ball >= rounds {
					cancel() // wake the partner
					return
				}
				fmt.Println(name, ball)
				select {
				case out <- ball + 1:
				case <-ctx.Done():
					return
				}
			}
		}
	}

	wg.Add(2)
	go player("ping", ping, pong)
	go player("pong", pong, ping)
	select { // serve
	case ping <- 0:
	case <-ctx.Done():
	}
	wg.Wait()
}

What does this print?

func main() {
	ch := make(chan int)
	go func() { v := <-ch; ch <- v + 1 }()
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
}

Answer: fatal error: all goroutines are asleep - deadlock!. The goroutine receives 1 and then blocks sending 2 back. Main is blocked sending 2 at the same moment. Both goroutines are trying to send on the same unbuffered channel and nobody is receiving. This is why ping-pong uses two directional channels. Be aware that the runtime only reports a deadlock when every goroutine is blocked. In a real server a stuck pair of goroutines just leaks silently.

More on Classic Concurrency Coding Problems

All 16 Classic Concurrency Coding Problems questions