Abhinav Gupta   About

Go Antipatterns: Channels that Fire Once

Introduction

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 presents a convoluted example of this.

// Worker does some 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() {
    // It takes us one second to get ready.
    time.Sleep(time.Second)
    fmt.Println("Worker is ready.")
    ready <- struct{}{}

    fmt.Println("Worker is working.")
  }()
  return 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.

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, causing the other to hang.

To show this, we switch main() to spawn two more Waiters. We use sync.WaitGroup to wait for them to finish.

func main() {
  ready := Worker()

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

  wg.Wait()
}

If you run this, the output will be something 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!
[...]

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.

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.

Conclusion

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

Written on Mar 29, 2019. Last modified on Apr 7, 2019.