This is a post in the metronome-cacophony series.
Other posts in this series:

Metronome's Cacophony (1/14) - concurrency in Go - Introduction

Introduction

The times of PCs with a single-core CPU seem to be gone. Single-core processors still appear in special use cases but, in Personal Computing, they are pretty much extinct. We exhibit constant drive for bigger, faster and richer. If I was buying a new PC, I wouldn't consider single-core CPU, unless it was clocked at extraordinarily high frequency.

At this moment however, we can't get our CPUs to be clocked at much faster speeds due to a number of factors, power wall being one and speed constraints at a sub-component level being another. You can find more info about it here.

To gain speed we add more cores instead, as it is easier and cheaper. The number of cores is well advertised, Intel Core X-series is stating "first 18-core processor" on their products page, while nVidia's Titan Xp GPU key features section states "CUDA Cores: 3840" as the first item. Impressive numbers, but why does it matter?

How number of cores can improve our experience?

If you think for a moment what you do on your PC at any time, it will probably involve having multiple browser tabs open, music playing in the background, social media feeds, email client and instant messenger constantly keeping you in touch with the world, anti-virus scanning your drives. It is all happening simultaneously. It would be so limiting if you had to choose whether to listen to music or browse the Internet, wouldn't it? Or if cute kittens video was pausing every time a social media feed was pulled.

Hiccups like that sometimes happen though. Such undesirable effects stem mainly from our code.

The operations that take considerable amount of time should not make the rest of the application features grind to a halt. I've seen many Android apps that didn't stick to that principle, where the OS had to serve the ANR notice. In many cases it is enough of a reason for me to uninstall such app.

This usually happens when the main thread itself is given long-running, blocking tasks. For example, on Android devices this thread is called UI thread and is responsible for everything that happens on-screen. When it gets busy doing other tasks, the app freezes. It indicates poor utilisation of concurrent primitives, which exist to alleviate such issues. Despite mentioning Android specifically here, the issue pertains to most other platforms as well.

As developers, we better learn how to avoid this. There's no excuse not to. In this day and age software languages give you plenty of tools to facilitate concurrent execution and hardware architecture even supports true parallelism.

This is where the number of cores comes in. It enables parallelism. With parallelism you have the ability to execute the tasks at the same time rather than sequentially. By utilising concurrent approach your application will be able to deal with multiple tasks at a time and leverage parallelism to its full extent.

This series of posts is about building a concurrent solution in Go, a very capable language in the field of concurrency (and others as well).

Concurrency

I have used a key term "concurrency" above, that I haven't explained.

concurrency

Concurrency is the composition of independently executing computations.

Concurrency is a way to structure software, particularly as a way to write clean code that interacts well with the real world.

It is not parallelism.

-- by Rob Pike in "Go Concurrency Patterns"

This definition comes from a great talk on Go's concurrency by Rob Pike, one of the Go's authors. Link to slide deck in the footnote of the quote. Associated video on Youtube.

Unfortunately, the definition itself is pulled out of context of the talk and refers to "parallelism", which is undefined here. It is still the best definition I've come across. I will refer to parallelism later in the series.

As we move through this series I will try to explain new terms. Despite of my experience in software development I have spent quite a bit of time traversing WWW to make my mind up about the best way to formally define some of them. Judging by the online debates that they sparkle, I'm reassured that I'm entering one of the most confusing areas of software engineering. I wish myself good luck.

Who is this series for?

This series of posts is addressed at people who want to learn more about concurrency primitives, in particular implementing concurrent solutions in Go. On few occasions I will try to project some of my Java experience onto the subject, so it maybe that Java developers will find some added benefit (albeit not big).

Many of the examples are quite naive, and you may perceive that I'm fooling you around. In such case you may be too experienced already to benefit from this series. Maybe just skip to last few examples to see if you can learn anything from it.

Prerequisites

I assume some basic level of understanding of Go's syntax. It is beyond the scope of this series to go into much detail. Whenever I realised it may be beneficial, I provided some links to third-party's supporting material.

Format

I decided to go through the motions of developing an example application, a metronome's engine. While at it, I will be catering for changing business requirements, addressing non-functional requirements and bugs. Each of the early iterations will be focusing on one or more issues arising from previous one. Last iterations will show alternative implementations, fit for purpose, demonstrating use of few concurrency primitives that Go makes available for you.

I will use timeline charts (courtesy of vis.js) hoping that visualising the execution of an application may help you to understand what's going on. In the posts I'm supplying just a static image of the chart. Click-through it, and you will be sent to an interactive version, where you can zoom, pan, read tooltips and highlight individual items.

All supporting code is on GitHub.

Running the examples

For linux:

1cd <work_dir>
2git clone git@github.com:outo/metronome.git 
3cd metronome
4go run cmd/main.go

The console output will contain the location of the HTML report with charts.

All about that rhythm!

The application's requirements

metronome

/ˈmɛtrənəʊm/

noun

a device used by musicians that marks time at a selected rate by giving a regular tick.

-- by Google


cacophony

/kəˈkɒf(ə)ni/

noun

a harsh discordant mixture of sounds. "a cacophony of deafening alarm bells"

-- by Google

Functional requirements

We are about to deliver an engine enabling other developers to create metronome application of their desire.

Its purpose is to handle the complexity around timing of individual beats, so that the consuming developer can focus on the bespoke part.

The timing, described as beats per minute (bpm), is one of the input parameters. It will accept values ranging (0, 60000] (from 0 excl. to 60000 incl.).

Because it is an engine for a metronome, rather than a metronome itself, it will allow you to supplement appropriate implementation performing beats (parameter named performer).

This contract is specified on GitHub but I've placed it here for your convenience:

1type Metronome func(bpm param.Bpm, performer BeatPerformer)

where

1package param
2...
3type Bpm int
4...

and

1type BeatPerformer func(beatCount, volume int)

BeatPerformer will accept beatCount (counting beats from 0 towards positive infinity) and volume (ranging [0, 100]).

Rationale: The beat's representation is understood to vary slightly, depending on its position in the sequence. beatCount allows to distinguish the start of measures and perform sequences such as "Tick, Tock, Tick, ..." (i.e. even, odd, even, odd) or "Tick, Tock, Tock, Tock, Tick, Tock, ..." (i.e. for remainder of dividing beatCount by 4).

volume will be populated with a value representing an actual ambient sound level. Thanks to that the implementer of BeatPerformer can choose to counteract high background noise with more, but relevant noise (and eventually contribute to the deafness of the user).

Yes, completely made up case. The choice is good enough for demonstrating the aspects I wanted to.

Non-functional considerations

The bpm parameter allows to specify very high values (i.e. high beat frequency). It is capped at 60000 BPM (1000 beats per second) which even the fastest Rave performers will treat as a joke. At high speeds, the timing will be slightly off. I'm expecting the total execution time to hold (within reason), but individual intervals might float. That is expected, as I'm not running the examples on a real-time system.

Although there is no requirement for producing audible or visual beats we need to see what is happening under the covers. Otherwise it would be hard to tell if the application does what it was asked to do. Testing would be one way. However, I am on a mission to teach you a lesson so I decided to track the events and output them as a chart.

BeatPerformer will implement an actual beat (sound, visual, haptic feedback, etc). The performance of the engine will be impacted if the beat performing is a long-running, blocking task. I've decided to leave it to the consumer developer to address.

In order to provide an ambient sound amplitude to the performer, measuring step has to be performed. That's one part of contract I have omitted in the snippet above:

1type VolumeMeter func() (volume int)

It is a simple function which just provides current ambient volume level. More about specifics of VolumeMeter implementation can be found in the appendices.

Let's stop here for now.


This is a post in the metronome-cacophony series.
Other posts in this series: