5

I don't understand how the Done() channel in context.Context can work as intended. The module documentation (and source code using it) relies on this pattern:

select {
case <-ctx.Done():
    return ctx.Err()

case results <- result:
}

The channel return by Done() is closed if the Context is canceled or timed out, and the Err() variable holds the reason.

I have two questions regarding this approach:

  1. What's the behavior of select when a channel is closed? When and why is the case entered? Does the fact that there's no assignment have relevance?

  2. According to the language reference:

    If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.

    If the choice is random, how does that pattern guarantee that I won't be sending results down a pipeline when the Context is canceled? I would understand if the cases were evaluated in declaration order (and closed channel cases were selected).

If I'm completely off the track here, please explain this to me from a better angle.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
salezica
  • 74,081
  • 25
  • 105
  • 166
  • “how does that pattern guarantee that I won't be sending results down a pipeline?”, no pattern can, that’s the nature of concurrency. – JimB Dec 06 '20 at 21:00
  • You can have some guarantee for the "how does that pattern guarantee that I won't be sending results down a pipeline" : it depends on how you created `result`. If `result` is unbuffered, the sending operation will only proceed if some receiving operation resolves. If `result` has a buffer, it may contain one (or more) value(s) ; however, you still have the guarantee that no value will be popped out from `result` if the branch that is executed in your `select` statement is the `<-ctx.Done() :` one. – LeGEC Dec 06 '20 at 23:33
  • 1
    Except it may be getting canceled the moment before it’s sent, or is ready to send right after you check the buffered channel. The cutoff is always arbitrary, and you just have to accept that you might have “close calls” if you’re not going to wait for a response, and it doesn’t really matter. – JimB Dec 06 '20 at 23:39

1 Answers1

9

This case:

case <-ctx.Done():

Has the communication op:

<-ctx.Done()

It's a receive from a channel. Spec: Receive operator:

A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.

So when the channel returned by ctx.Done() is closed, a receive from it can proceed immediately. Thus control flow can enter into this case.

If the other case (results <- result) can also proceed when the context is cancelled, one is chosen (pseudo-)randomly, there is no guarantee which one it will be.

If you don't want to send a value on results if the context is already cancelled, check the ctx.Done() channel before the select with another non-blocking select:

select {
case <-ctx.Done():
    return ctx.Err()
default:
}

select {
case <-ctx.Done():
    return ctx.Err()

case results <- result:
}

Note that you must add a default branch to the first select, else it would block until the context is cancelled. If there is a default branch and the context is not yet cancelled, default branch is chosen and so the control flow can go to the second select.

See related questions:

Force priority of go select statement

How does select work when multiple channels are involved?

icza
  • 389,944
  • 63
  • 907
  • 827
  • I'm not sure I understand how the first `select` helps. If another routine in a different thread closes the channel between statements, am I not in the same situation? – salezica Dec 07 '20 at 00:12
  • 2
    The first `select` helps that if the context is already cancelled, you will not attempt to send a value on `results`. Without that if the context is already cancelled when the `select` is executed, there is no guarantee which case is selected if both are ready to proceed. – icza Dec 07 '20 at 07:37