Favor one communication (chan) in select

Viewed 101

I know that if there are more than one "communications" that can proceed in a select statement that one is chosen randomly. I am trying to find an alternative approach that can prefer one "communication" over another.

The background is that I am sending values in a go-routine on a channel which is killed using a context. When I kill it I want the channel to be immediately closed but currently the code will sometimes send a final value on the chan before closing it.

Here is a simplified version of the code:

   ctx, cancel := context.WithCancel(context.Background())
   ch := make(chan int)

   go func() {
      defer close(ch)
      for i := 1; ; i++ {
         select {
         case <-ctx.Done():
            return
         case ch <- i:
         }
      }
   }()

   print(<-ch)
   print(<-ch)
   cancel()
   print(<-ch)
   print(<-ch)

This sometimes prints 1200 but usually 1230. Try it on the playground

Are there any ideas on how to reorganise the code to favour the first case? (Ie have it always print 1200.)

2 Answers

This does not appear to be possible because cancel() is not a blocking operation in the main goroutine. Because of this, when select unblocks there can be multiple cases available and there is no way to favor one channel over the other. Any kind of check-channel-then-write scheme will be racy because the context can be canceled after the check.

Using a done channel and writing to it instead of context cancellation will work because writing to a done channel will be a blocking operation for the main goroutine, and select will always have one active case.

Note that this is an updated answer as there were problems with the original.

As pointed out by others you cannot avoid a race condition without additional synchronisation. You could use a Mutex but sync.Cond seems like a good fit. In the code below the receiving goroutine signals that it has received the value from the chan. It cancels the context before signaling (using Cond.Signal) and the sending goroutine waits for the signal. This avoids the race condition since the context status is updated before it can be checked.

ctx, cancel := context.WithCancel(context.Background())
ch := make(chan int)
cond := sync.NewCond(&sync.Mutex{}) // *** new

go func() {
    defer close(ch)
    cond.L.Lock()                   // *** new
    defer cond.L.Unlock()           // *** new
    for i := 1; ; i++ {
        ch <- i                     // *** moved
        cond.Wait()                 // *** new
        if ctx.Err() != nil {       // *** changed
            return
        }
    }
}()

print(<-ch)
cond.Signal()                      // *** new
print(<-ch)
cond.Signal()                      // *** new
print(<-ch)
cancel()
cond.Signal()                      // *** new
print(<-ch)
cond.Signal()                      // *** new

This is the simplest way I can see that the receiving goroutine will not receive any more values on the channel after it has canceled the context.

Try it on the Playground

Related