Select

midpeter444 edited this page Feb 2, 2013 · 7 revisions

select

Go comes with a ready-made control structure called select. It provides a shorthand way to specify how to read from the next ready among multiple channels, as well as allow for timeouts and non-blocking behavior (via a "default" clause). It looks like a switch/case statement in Go, but is different in that all paths involving a channel are evaluated, rather than just picking the first one that is ready.

The select statement is a central element in writing idiomatic Go concurrent applications. In fact, Rob Pike, in his Google IO 2012 presentation) on Go said:

"The select statement is a key part of why concurrency is built into Go as features of the language, rather than just a library. It's hard to do control structures that depend on libraries."

While this may be hard to add with a library in many languages, it is not so difficult with a Lisp. The go-lightly library provides a four variants of the select "control structure" in Clojure.

But first, let's look at how it works in Go:

select {
case v1 := <-c1:
    fmt.Printf("received %v from c1\n", v1)
case v2 := <-c2:
    fmt.Printf("received %v from c2\n", v2)
}

This select evaluates two channels and there are four possible scenarios:

  1. c1 is ready to give a message, but c2 is not. The message from c1 is read into the variable v1 and the code clause for that first case is executed.
  2. c2 is ready to give a message, but c1 is not. v2 then is assigned to the value read from c2 and its code clause is executed.
  3. Both c1 and c2 are ready to give a message. One of them is randomly chosen for reading and execution of its code block. Note this means that you cannot depend on the order your clauses will be executed in.
  4. Neither c1 nor c2 are ready to give a message. The select will block until the first one is ready, at which point it will be read from the channel and execute the corresponding code clause.

select statements in Go can also have timeouts or a default clause that executes immediately if none of the other cases are ready. Examples are provided in the go-examples directory the go-lightly repo.


select in go-lightly

The go-lightly library provides a couple of variations on the select operator:

  • select, which reads from the first available channel and returns that value
  • selectf, which is a control structure like Go's select, where you provide a function to call based on which channel is read from
  • select-nowait, which is a variant of select except returns immediately if no channel has a ready value
  • select-timeout, which is a variant of select that takes a timeout value to return a sentinel timeout value if the timer goes off before a channel has a ready value

select and select-nowait

The select function in go-lightly is modelled after the sync function in the Racket language (discussed in one of my go-lightly blog entries in more detail). You pass in one or more channels and/or buffered channels and it performs the same logic outlined above to return the value from the next channel when it is available.

(def ch (go/channel))
(def buf-ch1 (go/channel 100))
(def buf-ch2 (go/channel Integer/MAX_VALUE))

(let [msg (go/select ch buf-ch1 buf-ch2)]
  ;; do something with first message result here
  )

If you don't want to block until a channel has a value, use select-nowait, which takes an optional return sentinel value (in the last arg position) if no channel is ready for reading. It returns nil if no channel is ready and no sentinel return value is provided.

user=> (select-nowait ch1 ch2 ch3 :bupkis)
:bupkis
user=> (select-nowait ch1 ch2 ch3)
nil

select-timeout

select-timeout is a variant of select that takes a specified timeout value. See the #timeouts section for details.


selectf

There are some situations in which you not only want to read the next ready value from multiple channels, but you need to know exactly which channel is was read from. (The load balancer example in the clj-examples directory shows this scenario.) selectf acts like Go's select control structure in that you provide pairs of arguments: the first one is the channel to read from and the second is the function to invoke if it is read. That function takes one argument, which is the value read from the channel.

You can also provide the keyword :default as a first argument with an accompanying function to invoke (which takes no args). Like Go's default selector, this will be called immediately if no channel has a ready value.

You can provide it a timeout-channel to avoid unbounded waits:

(go/selectf ch1 (fn [v] (callme v "channel 1"))
            ch2 (fn [v] (callme v "channel 2"))
            (go/timeout-channel 100) #(println "timeout:" %))

You can provide a default clause to return immediately if no channel has a ready value:

(go/selectf ch1 (fn [v] (callme v "channel 1"))
            ch2 (fn [v] (callme v "channel 2"))
            :default #(println "no values ready"))
selectf returns a value

selectf returns the value returned by the handler function. This can be useful to compose selectf with a looping construct. See the learn at the REPL part of the wiki for an example of this. The sleeping barbers example in the clj-examples directory also demonstrates this.


read preferentially from preferred channels

Finally, you can also set one or more channels as "preferred" so that select will preferentially read from them over non-preferred channels. This is not a feature of Go's select.

In that case the logic for select in go-lightly changes to:

  1. Evaluate all preferred channels.
    1. If only one is ready, take from it and return its value
    2. If more than one is ready, choose randomly among them, take from it and return its value
  2. Evaluate all non-preferred channels.
    1. If only one is ready, take from it and return its value
    2. If more than one is ready, choose randomly among them, take from it and return its value
  3. If none is ready, then:
    1. If called with select, wait a short sleep and go to step 1 above
    2. if called with select-nowait return nil immediately
    3. if called with select-timeout return nil if has timed out, otherwise do short sleep and return to step 1 above

Note that a TimeoutChannel is a preferred channel by default. See the status section for more details on preferred.