welcome at SebWalak.com

Metronome's Cacophony (4/14) - concurrency in Go (GoLang) - Goroutine

Asynchronous approach

Another big word, another definition

asynchronous

/eɪˈsɪŋkrənəs/

adjective

  1. not existing or occurring at the same time.
  2. COMPUTING, TELECOMMUNICATIONS
    controlling the timing of operations by the use of pulses sent when the previous operation is completed rather than at regular intervals.

– by Google

In the context of programming models, I will translate it as “disconnected”. It means that in case of two asynchronous tasks processing of any of them can start before the other has finished. It does not automatically mean that the tasks will be executed at exactly same time though (which is defined as parallelism).

Drinks analogy.

Synchronised execution first, to build contrast. At a wedding reception, a glass of “champagne” is served to each of the guests. They lift the glasses to raise a toast. Delivery of drinks has to be synchronised as well as the moment of raising glasses or you might as well not do it at all.

At a pub, when the round is on you, you ask your mates what’s their choice and relay it to the barman. As the ready drinks appear, you may take some back to the table and come back for more. Some people would start drinking while you are still stood at the bar. The moment they can enjoy their drinks isn’t the same for all and resembles asynchronous execution.

Common deliverable for both scenarios is each person consuming a drink. The difference between them was possible because of relaxed social protocol, allowing people to start the task of drinking without waiting for others. Obviously, without early release of ready drinks at the bar it would not be possible.

Similarly in programming, in order to solve a problem asynchronously, you need to break down a task into pieces that will be slightly disconnected from each other. That relaxation requires extra complexity to deal with because of the task split, transfer of deliverables and subsequent assembly of them. However, it allows for tasks to be progressing at uneven pace without impacting each other.

Goroutine

Most programming languages I’ve used, allows you to build concurrent solutions and operate on low-level primitives - threads. It offers great deal of control, but also drastically increases complexity. The patterns of usage are so similar that I class this knowledge as transferable between many languages, to an extent.

Go is definitely built with concurrency in mind but it does not allow for control of thread primitives. The most basic concurrency primitive in Go is goroutine. It is an independent, concurrent thread of control, but is not synonymous to a thread. Think of it as a very lightweight thread which can be spawned at a very low cost to the operating system.

Note: Goroutines are extremely cheap because the tasks are multiplexed onto a set of OS threads. So they are not threads but they utilise threads to enable you to perform concurrent tasks. They use only about 2kB execution stack to start with, but the stack grows when needed. Having thousands of active goroutines at any time is not uncommon.

Java analogy: Because of the lightness of goroutine I wouldn’t compare it to spawning a thread in Java. It would have to be ExecutorService which distributes the workload of executing tasks over a thread pool. It is waaaay more verbose and unpleasant to use. Kind of important if your software needs to handle work concurrently and somebody has to maintain it later.

I welcomed Go’s approach with open arms. Why should the concurrent code be complicated?

Anyway, I feel Goroutine requires an extremely simple example to illustrate what it is about. Firstly, let’s build some contrast with the snippet which does NOT use Goroutines:

package main

import (
   "log"
)

func main() {
   // sequence of activites I want to perform
   log.Println("walk into bathroom")
   log.Println("undress")
   log.Println("take shower")
   log.Println("put pyjamas on")
   log.Println("hug your parents and say good night")
   
   log.Println("program ends")
}

Output: (I have used logger here so that we can see timings, important in the subsequent examples)

2018/05/01 17:59:30 walk into bathroom
2018/05/01 17:59:30 undress
2018/05/01 17:59:30 take shower
2018/05/01 17:59:30 put pyjamas on
2018/05/01 17:59:30 hug your parents and say good night
2018/05/01 17:59:30 program ends

Process finished with exit code 0

I hope the output does not come as a surprise. Lines from 9 to 13 will execute one after another, synchronously, each printing activity name.

How do we make Goroutines though? Just precede any function with go keyword. That’s it!

Note: It is true if our only goal is to make piece of code run alongside the main code. That means we are ignoring bunch of important aspects. You will see what I mean in the next few snippets.

Let’s rewrite our shower sequence with Goroutines:

package main

import (
   "log"
)

func main() {
   // sequence of activites I want to perform
   go log.Println("walk into bathroom")
   go log.Println("undress")
   go log.Println("take shower")
   go log.Println("put pyjamas on")
   go log.Println("hug your parents and say good night")

   log.Println("program ends")
}

Output:

2018/05/01 17:59:30 program ends

Process finished with exit code 0

Whaaat?!

That’s actually expected.

Each invocation in lines 9 - 13 is going to start a new Goroutine. When you start a Goroutine the main code does not wait for Goroutine’s completion but immediately moves onto the next line. Because we just create few Goroutines the flow will zip instantly through them, creating each, and then reaching the end of a program, before any of Goroutines has a chance to print any text.

To give it a chance it is enough to put a pause

time.Sleep(time.Second)

before we print “program ended”. Example output:

2018/05/01 18:43:41 put pyjamas on
2018/05/01 18:43:41 walk into bathroom
2018/05/01 18:43:41 take shower
2018/05/01 18:43:41 undress
2018/05/01 18:43:41 hug your parents and say good night
2018/05/01 18:43:42 program ended

Process finished with exit code 0

It is important to notice the ordering anomaly here. Be careful! Unless you don’t mind taking a shower in your pyjamas and then undressing before you hug your parents and say good night to them.

Imagine if such quality code was deciding about your finance or life. Yikes!!

If you feel this is some textbook example detached from reality, let’s see.

Naive solution with Goroutines

In the following, naive, example I have wrapped volume measurement and beat performing in an anonymous function (function without a name), which is launched as Goroutine.

Note: I came across term “inline function” as a synonym for an “anonymous function”. I am trying to stick to term “anonymous” due to Go’s compiler optimisation features called “function inlining” and being something completely different to “anonymous function”. It may be ok in JavaScript world.

The main code is just waiting for the correct time and launching new Goroutines.

func(bpm param.Bpm, performer metronome.BeatPerformer) {  
  
   ticker := time.NewTicker(bpm.Interval())
   defer ticker.Stop()
     
   for beatCount := 0; beatCount < numberOfBeats; beatCount ++ {  
  
      go func(beatCount int) {  
         volume := volumeMeter()  
         performer(beatCount, volume)  
      }(beatCount)  
  
      <-ticker.C  
   }  
     
}

Note: be extremely vigilant when defining a body of an anonymous function inside another function, especially when you are intending to run anonymous function as a Goroutine. If I didn’t include beatCount variable in the signature of the anonymous function (which also means I wouldn’t have to pass beatCount as a parameter) I wouldn’t be warned by a compiler. There already is beatCount variable defined as part of for-loop and that could be a valid operation. However, because we are running the anonymous function as a goroutine, the beatCount variable that it would have access to, would be constantly changing within the scope of the Goroutine. This change will be caused by the for-loop outside the anonymous function. We are saying that the anonymous function is closing over the lexical scope of the enclosing function (aka closure). This seems to be very common bug made by fresh Gophers. It usually leads to race conditions and will cause a host of serious problems. Closures induced bugs was my least favourite during work with one of the Go’s BDD frameworks, Ginkgo.
By defining it like the snippet shows, we are avoiding this issue, because we are passing a value of a beatCount as it was at the time of function creation (i.e. it won’t be affected by for-loop within the body of that Goroutine). It can get confusing and I have introduced quite few errors because of this (also, pure Java does not support this concept so it may strike you when you expect it least). If I didn’t explain this well and you fancy having a play with it please have a look at Go by example and have a go A Tour of Go.

Here’s a diagram of what happened:

Asynchronous - naive attempt

Asynchronous - naive attempt

On the above diagram I’ve introduced red, vertical line which marks end of program.

Keeping in mind the learnings of the above examples it is expected that the beats ordering is disturbed (click through the diagram to inspect) and that trailing beats have never been performed.

Note: With the end of program all Goroutines that were still running, finish abruptly. That can lead to inconsistencies in data, memory leaks or poor UX.

Now, I would like to introduce another concurrency primitive, that will let you wait for all Goroutines to finish their work. For metronome’s beats it does not matter much, but for transferring files it would render the application useless if we kept ending file transfer before last byte was sent and acknowledged.

Thumbnail