Safe to use timeout channels and alts! without priority?


#1

Hi,

I have a question about alts! and timeout channels, as introduced in chapter 10.

Starting in exercise 4, we combine timeout channels with alts!, in order to create code which will handle values from value channels up until an interval of time has elapsed. The code has this structure:

(let [[val ch] (alts! [timeout-chan chan-a chan-b])]
  (if (= ch timeout-chan)
    ;; if we took from timeout-chan, then
    ;; timeout-chan did not block,
    ;; so the time interval has elapsed, so we are done
    (println ("Time's up and we're done!"))

    ;; if we did not take from t
    ;; then timeout-chan blocked (so the time interval has not elapsed)
    (do
      (println "Handling a value from chan-a or chan-b here")
      (handle-value-and-channel val ch))))

I don’t understand why this works reliably.

The video explanation says that alts! will “take from the first one to have a value ready”, meaning I presume the “first one” which is ready rather than the first in terms of vector order. That’s consistent with the alts! docs which say that “if more than one port operation is ready a non-deterministic choice will be made”, and with the later discussion of :priority.

But if that’s the case, and if the other channels are always ready (because they are fed by an unblocked producer), then it seems like we do not have a guarantee that alts! will ever try to take from the timeout channel.

It could just (non-deterministically) keep choosing the value channels, and never try the timeout channel, and then it’d never discover the the timeout channel would return a nil value to <!, and then we’d never find out that the time interval has elapsed. In other words, I wonder why the code above could not be more accurately commented as follows:

(let [[val ch] (alts! [timeout-chan chan-a chan-b])]
  (if (= ch timeout-chan)
    ;; if we took from timeout-chan, then
    ;; timeout-chan did not block,
    ;; so the time interval has elapsed, so we are done
    (println ("Time's up and we're done!"))

    ;; if we did not take from t
    ;; then 
    ;; 1. EITHER timeout-chan blocked (so the time interval has not elapsed)
    ;; 2. OR `alts!` did not happen to try to take from timeout-chan
    (do
      (println "Handling a value from chan-a or chan-b here")
      (handle-value-and-channel val ch))))

I realize this is probably an unlikely case when we only have two value channels, but if you had thousands of channels, maybe one for every concurrent client for some kind of server, then it seems like it could be an issue. Does alts! randomly choose what channel to “try” to take from, until one doesn’t block? If so, and if there were one timeout channel and thousands of value channels, then you’d have to wait quite a while until the timeout channel happened to be picked.

In other words, I’m wondering why we are not obliged always to use :priority true to ensure that the timeout channel is checked before the value channels.

Thanks,
Alexis


#2

Hi Alexis,

Great question. It shows that you are understanding a lot!

I’ll try to keep the explanation concise.

When alts! runs, it does not “try” taking from the channels randomly. Instead, it randomly chooses one of the channels that are ready, or, the one channel that is ready if it’s only one. If no channels are ready, the go block is parked. So if two channels are both always full (or closed), it will randomly select one of them. It’s a different random choice each time through the loop, so it becomes statistically more likely over time that the timeout will be chosen, though never certain. If you really want to guarantee that you stop as soon as possible after the timeout is over, you should use :priority with the timeout first. This is what I do in my code when I want a “hard stop”.

However, sometimes I want a “soft stop”, meaning if there’s a value on chan-a or chan-b, I want to process them, even if the timeout is completed. This is often the case when I’m making a web request and I want to wait at most n milliseconds for it. If, when alts! finally unparks the go block, there is a value waiting, by all means, use it. In that case, I use :priority but I order the channels the other way, value channel first, then timeout.

Eric


#3

Great! Thanks, that makes a lot of sense.

It sounds like, in practice, you do always want to set :priority, but the question is really whether you want to put the timeout channel first (to stop as soon as possible) or last (to finish all pending work first).

This is very handy to know!