Metronome's Cacophony (4/14) - concurrency in Go - Goroutine
Asynchronous approach
Another big word, another definition
asynchronous
/eɪˈsɪŋkrənəs/
adjective
- not existing or occurring at the same time.
- 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:
1package main
2
3import (
4 "log"
5)
6
7func main() {
8 // sequence of activites I want to perform
9 log.Println("walk into bathroom")
10 log.Println("undress")
11 log.Println("take shower")
12 log.Println("put pyjamas on")
13 log.Println("hug your parents and say good night")
14
15 log.Println("program ends")
16}
Output: (I have used logger here so that we can see timings, important in the subsequent examples)
12018/05/01 17:59:30 walk into bathroom
22018/05/01 17:59:30 undress
32018/05/01 17:59:30 take shower
42018/05/01 17:59:30 put pyjamas on
52018/05/01 17:59:30 hug your parents and say good night
62018/05/01 17:59:30 program ends
7
8Process 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:
1package main
2
3import (
4 "log"
5)
6
7func main() {
8 // sequence of activites I want to perform
9 go log.Println("walk into bathroom")
10 go log.Println("undress")
11 go log.Println("take shower")
12 go log.Println("put pyjamas on")
13 go log.Println("hug your parents and say good night")
14
15 log.Println("program ends")
16}
Output:
12018/05/01 17:59:30 program ends
2
3Process 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
1time.Sleep(time.Second)
before we print "program ended". Example output:
12018/05/01 18:43:41 put pyjamas on
22018/05/01 18:43:41 walk into bathroom
32018/05/01 18:43:41 take shower
42018/05/01 18:43:41 undress
52018/05/01 18:43:41 hug your parents and say good night
62018/05/01 18:43:42 program ended
7
8Process 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.
1func(bpm param.Bpm, performer metronome.BeatPerformer) {
2
3 ticker := time.NewTicker(bpm.Interval())
4 defer ticker.Stop()
5
6 for beatCount := 0; beatCount < numberOfBeats; beatCount ++ {
7
8 go func(beatCount int) {
9 volume := volumeMeter()
10 performer(beatCount, volume)
11 }(beatCount)
12
13 <-ticker.C
14 }
15
16}
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:
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.
Other posts in metronome-cacophony series
- Introduction
- Basic synchronous solution
- Ticker
- Goroutine
- WaitGroup
- Sharing state
- Atomicity
- Mutex
- Channels introduction
- Channel select
- Goroutines and channels
- Solution with channel
- Videos and final word
- Appendices