Abhinav Gupta | About

Go Antipatterns: Channels that fire once

Concurrent Go code often requires signaling to other goroutines that a certain event has occurred. Posting a sentinel value to a channel is a common approach to doing this.

The following is a convoluted example of this.

// Worker does work, before which it needs to get ready.
// The returned channel receives a sentinel value when the
// Worker is ready to do the work.
func Worker() (ready chan struct{}) {
  ready = make(chan struct{})
  go func() {
    time.Sleep(time.Second) (1)
    fmt.Println("Worker is ready.")
    ready <- struct{}{}

    fmt.Println("Worker is working.")
  }()
  return ready
}
  1. Pretend it takes the worker a second to become ready.

// Waiter wants to wait for Worker to be ready before doing
// some work.
func Waiter(ready chan struct{}) {
  fmt.Println("Waiter does not need Worker yet.")
  time.Sleep(100*time.Millisecond)

  // Wait for Worker.
  fmt.Println("Waiter needs Worker now.")
  <-ready

  fmt.Println("Waiter is running.")
  time.Sleep(100*time.Millisecond)
}

func main() {
  done := Worker()
  Waiter(done)
}

The output of the program above is,

Waiter does not need Worker yet.
Waiter needs worker now.
Worker is ready.
Worker is working.
Waiter is running.

1. Problem

The primary issue here is that it’s impossible for us to have more than one Waiter. Introducing another Waiter will cause the two to race for the done signal—​one will win and the other will freeze.

To demonstrate this, we switch main() to spawn two more Waiters.

func main() {
  ready := Worker()

  var wg sync.WaitGroup (1)
  for i := 0; i < 3; i++ {
    wg.Add(1)
    go func() {
      Waiter(ready)
      wg.Done()
    }()
  }

  wg.Wait()
}
  1. We use sync.WaitGroup to wait for them to finish.

If you run this, the output will look roughly like the following.

Waiter does not need Worker yet.
Waiter does not need Worker yet.
Waiter does not need Worker yet.
Waiter needs worker now.
Waiter needs worker now.
Waiter needs worker now.
Worker is ready.
Worker is working.
Waiter is running.
fatal error: all goroutines are asleep - deadlock!
[...]

2. Solution

The solution to this is straightforward: Close the channel instead of posting to it. Reads from a closed channel return the zero value right away. All goroutines waiting for the signal will get notified when the channel closes.

ch := make(chan int)
close(ch)
fmt.Println(<-ch)  // 0
fmt.Println(<-ch)  // 0
fmt.Println(<-ch)  // 0

With the following change in Worker, the code above works as expected.

     fmt.Println("Worker is ready.")
-    ready <- struct{}{}
+    close(ready)

     fmt.Println("Worker is working.")

The output is,

Waiter does not need Worker yet.
Waiter does not need Worker yet.
Waiter does not need Worker yet.
Waiter needs worker now.
Waiter needs worker now.
Waiter needs worker now.
Worker is ready.
Worker is working.
Waiter is running.
Waiter is running.
Waiter is running.

2.1. Caveats

A channel cannot be re-used once closed. This makes this approach incompatible with events that occur more than once.

You cannot post information to a closed channel. This approach prevents posting information (like the output of a task) back to the waiters.

3. Conclusion

If you have a channel that receives a single sentinel value, you can switch to closing that channel instead.

Written on .