welcome at SebWalak.com

Metronome's Cacophony (10/14) - concurrency in Go (GoLang) - Channel Select

Asynchronous approach (ctd)

Channels (ctd)

Select statement

If you want to embrace Go’s channels, but are feeling constrained by all the blocking operations on them, select will make your day. It is part of Go’s syntax and - in its simplicity - it’s stunning how powerful it is. Link to documentation.

Non-blocking channel receive with select statement

package main

import "fmt"

func main() {
   words := make(chan string)
   select {
      case word := <-words:
         fmt.Println("word is:", word)
   default:
      fmt.Println("default case")
   }
}

will produce:

default case

Process finished with exit code 0

Above snippet attempts to receive a word from an empty channel words. As mentioned before, receive operation on an empty channel will block indefinitely. If you use select with default clause, it will allow you to attempt an operation on a channel without blocking penalty. If the channel cannot perform the operation in a non-blocking manner, select will … uhm … select default clause, execute whatever code is in default’s body and leave select statement immediately after.

Non-blocking channel send with select statement

An example for sending:

package main

import "fmt"

func main() {
   words := make(chan string)
   select {
      case words <- "hello":
         fmt.Println(`"hello" sent`)
   default:
      fmt.Println("default case")
   }
}

will produce

default case

Process finished with exit code 0

because the channel in the example is unbuffered and there is no receiver attached to it at the time of sending. We already know that sending in this case (or sending to a channel with no more capacity) would normally block.

Note: if you remove default case from the two examples above you will loose all benefit of select. Both cases will behave like the non-select, blocking equivalents from previous section. Both will also produce you favourite - deadlock.

Channel closure detection with select

You have already seen a mechanism for verifying if the channel is closed. Same mechanism can be used with select:

package main

import "fmt"

func main() {
   words := make(chan string, 1)
   words <- "hello"
   close(words)

   select {
   case word, ok := <-words:
      fmt.Println("word is:", word, "ok:", ok)
   }

   select {
   case word, ok := <-words:
      fmt.Println("word is:", word, "ok:", ok)
   }
}

will output

word is: hello ok: true
word is:  ok: false

Process finished with exit code 0

Notice that despite of closing the channel before we even proceed to select clause, the first receive will return true value. To reiterate, second return value from <- channel operation will return false when the channel is closed and empty.

What about sending?

package main

func main() {
   words := make(chan string, 1)
   close(words)

   select {
   case words <- "hello":
   default:
   }

}

will produce:

panic: send on closed channel

goroutine 1 [running]:
main.main()
	/home/seb/.GoLand2018.1/config/scratches/scratch_25.go:8 +0x76

Process finished with exit code 2

because sending on channel after close() has been invoked against it, will always panic. Not even mighty select statement will help here.

Multiple case clauses/channels with select

You’ll find yourself coordinating communication between multiple channels. select is flexible enough to allow for this to happen in a single statement.

As an illustration, let’s imagine a factory with four assemblers and two conveyor belts which will take away ready products. Assembler is given an instructions to assemble a product. Assembler can be busy with a single product at a time. Once assembler finishes the task, ready product is put on the closest conveyor belt.

package main

import (
   "fmt"
)

func main() {
   type AssemblyInstructions struct{}
   type AssembledProduct struct{}

   assemblerA := make(chan AssemblyInstructions, 1)
   assemblerB := make(chan AssemblyInstructions, 1)
   assemblerC := make(chan AssemblyInstructions, 1)
   assemblerD := make(chan AssemblyInstructions, 1)

   conveyorBelt1 := make(chan AssembledProduct, 1)
   conveyorBelt2 := make(chan AssembledProduct, 1)
   conveyorBelt1 <- AssembledProduct{}
   conveyorBelt2 <- AssembledProduct{}

   attemptsLeft := 10
   for attemptsLeft > 0 {
      attemptsLeft --
      
      select {
      case assemblerA <- AssemblyInstructions{}:
         fmt.Println("assemble car")
      case assemblerB <- AssemblyInstructions{}:
         fmt.Println("assemble pen")
      case assemblerC <- AssemblyInstructions{}:
         fmt.Println("assemble soup")
      case assemblerD <- AssemblyInstructions{}:
         fmt.Println("assemble mind")
      case <-conveyorBelt1:
         fmt.Println("finished on belt 1")
      case <-conveyorBelt2:
         fmt.Println("finished on belt 2")
      default:
         fmt.Println("defaulted")
      }
   }
}

We have 6 channels, each with buffer of one. assembler* channels are empty while conveyorBelt* channels are prepopulated with one message each. select is used with all 6 channels, on 4 to send and on 2 to receive messages. We are going to invoke select 10 times to see how the situation develops as the saturation of channels changes.

Note: if you wonder if case clauses within select statement fallthrough to the next case upon match, the answer is no. Similarly, in Go’s switch statement they don’t, but in the switch you can use fallthrough to make them do. select does not come with one.

Java developers: the above note mentions falling through in switch clauses. In Java the clauses that match will fall through by default. That’s why break and return is used, to stop it. #pay-per-keystroke

The output:

assemble pen
finished on belt 2
assemble soup
assemble mind
finished on belt 1
assemble car
defaulted
defaulted
defaulted
defaulted

Process finished with exit code 0

Note: your output will most likely differ. If you run this enough times and you’ll eventually get 6! (factorial) possible outputs, with the “defaulted” always trailing.

Explanation of what has happened is as following.

On each select invocation, it will select only one of the clauses. The mechanism for selecting close will choose pseudo-randomly one of clauses that wouldn’t block.

Note: wait, what? That’s slightly undeterministic, isn’t it? It is done like that to not promote any of the single clauses over another one. In concurrent models this would be referred to as fairness. If execution is fair then all threads are roughly equally busy. Extreme case of unfair execution gives all the workload to one thread and the others never get time to do their job. This concurrent execution phenomenon is called starvation

On first iteration, none of the 6 operations would block. We can send any of the 4 messages (channels assembler* are empty and have a buffer of 1) and we can also receive a single message on any of the two prepopulated conveyorBelt* channels.

As per the output, coincidentally, the first clause won and we have sent the message on assemblerA channel. That channel is now full as the buffer has size 1 and nothing will ever receive messages from it. It means it won’t get selected again as this would block operation.

On second iteration the clause which receives from conveyorBelt2 won. We have received the only message that this channel had, and it is now empty. This clause won’t get selected again because receiving from an empty channel will block until the channel is closed, which does not happen here.

And so on, the pattern repeats until we get to the 7th iteration when all of the operations are blocking, in which case select picks default clause.

Until more messages is put on channels conveyorBelt* or messages are received from assembler* channels the ouput is going to remain the same.

Note: the above example contains quite a bit of repetition and hardcoded presumption as to how many channels there is. What if you want to serve an N-number of channels? I would consider changing the solution so that receiving happens from a channel merging the N-channels and writing to a channel that is then fanned out to N-channels. Also, I wouldn’t discredit simple for-loop, going with the spirit of Go’s simplicity. I’ve also seen a reflection based solution here, but I haven’t used it.

Channel sending/receiving timeout with select

Ability to impose timeouts on long-running tasks is essential to keep control of the system’s performance and UX. We rarely can take a no-compromise approach where we would expect the resources or messages to be readily available. Neither we can wait for completion indefinitely. Well, we often have no indication whatsoever as to what is the length of the task going to be.

In computing, delays are ubiquitous and a reminder of physical limitations of our world. Your code gets an advantage, if you are expecting and catering for delays, however minimal.

With the select syntax we can make conscious design decision about how much of a delay is acceptable for the system.

Armed with the knowledge about select you may even expect what the solution could be.

package main

import (
   "time"
   "log"
)

func main() {
   log.Println("start")
   complimentsChannel := make(chan string, 1)

   complimentsChannel <- "you've made fantastic dinner, thank you very much!"

   select {
      case response := <-complimentsChannel:
         log.Printf("received compliment: %q\n" ,response)
      case <- time.After(5 * time.Second):
         log.Println("too long wait for a compliment, attack verbally")
   }
   log.Println("end")
}

Function time.After() will create and return a channel which, after specified time, will have a single message sent to it by Go. We treat the above select case no different to a standard case receiving from two channels.

Output:

2018/05/01 22:34:51 start
2018/05/01 22:34:51 received compliment: "you've made fantastic dinner, thank you very much!"
2018/05/01 22:34:51 end

Process finished with exit code 0

output demonstrates that, in case one of the channel operations won’t block (compliment is ready to be received), it is performed immediately.

If you were to comment-out compliment sending in line 12, you would get this output:

2018/05/01 22:36:56 start
2018/05/01 22:37:01 too long wait for a compliment, attack verbally
2018/05/01 22:37:01 end

Process finished with exit code 0

Notice the times in log.

Initially the timing channel returned by time.After() and complimentsChannel are empty. Because there is no default case in the above select, it will just wait forever, until a message appears in any. After specified timeout message appears in timing channel, which makes this select end.

Thumbnail