welcome at SebWalak.com

Metronome's Cacophony (2/14) - concurrency in Go (GoLang) - Basic Synchronous Solution

Synchronous approach

Before we move on I will try to explain what the big word is

synchronous

/ˈsɪŋkrənəs/

adjective

existing or occurring at the same time. “glaciations were approximately synchronous in both hemispheres”

– by Google

Did it help? Did it resonate with you?

Well, for me it felt like the definition contradicts my understanding of the term in context of software development.

By the looks of it, many people are in the same boat, just look at this StackOverflow debate.

So, my take on it is that synchronous would translate as “connected” and most likely refers to the mechanism of coordinating tasks with each other. It would describe the dependency of subsequent task on the deliverable from preceding task. That dependency means that a task cannot start until previous has finished. Sometimes this deliverable is just the fact previous task completed. The key however is, that this exchange happens at the same time (or is connected).

In other words, synchronous execution of tasks would be an execution where the subsequent task’s start waits for the completion of a preceding task.

Note: this definition does not imply anything to do with the number of cores or threads. It is possible to execute two tasks in synchronous manner with each running on a separate thread. In fact using Go’s unbuffered channels on separate Goroutines gets you something like that (apart from the fact that you cannot force Goroutines to execute on separate threads). It is more about the way the dependency between them is managed.

In case you like analogies let me bring a food processing analogy of synchronous processing. Imagine a kitchen setting and that you are asked to chop all items for a salad. Another person is waiting for chopped items to appear in a bowl before seasoning and mixing can start. You are given carrots, leeks, potatoes, apples, a chopping board and a knife. You probably will choose to chop all carrots, then leeks, and so on, before passing it further. If we define one task as chopping and another as mixing then you have executed this synchronously as you wouldn’t progress onto mixing before chopping is finished.

As you can see this approach makes sense in certain situations.

In programming, this still is the standard way of solving problems when the execution time of individual lines of code is predictable and acceptably short. Examples could be concatenating first and last name, iterating over months in calendar or implementing “times” table for primary school, etc…

So, despite the much prolonged explanation above, synchronous execution is the most straightforward way of writing a program. One that you most likely will be familiar with, if you’ve done any programming.

Trivial scenario

The simplest (compact) metronome’s engine solution I can think of

func(bpm param.Bpm, performer metronome.BeatPerformer) {  
  
   //measure the volume before the beats are to be performed  
   volume := volumeMeter()  
  
   //loop over beatCount values ranging \[0, numberOfBeats)  
   for beatCount := 0; beatCount < numberOfBeats; beatCount ++ {  
 
      //delegation to beat performer which will (as an example) print Ticks and Tocks  
      performer(beatCount, volume)  
  
      //planned delay so that the beats appear at equal intervals 
      //Bpm.Interval() will perform simple calculation of interval length 
      // to match required bpm value. 
      time.Sleep(bpm.Interval())  
   }
}

It is an example of synchronous execution.

Every task in this snippet is executed after preceding one has finished. First we obtain the volume (line 4), then we perform the beat (line 10), wait (line 15) and repeat the performing and waiting until predefined number of beats is reached.

Note: You can see the moment we are obtaining a volume measurement but the body of volumeMeter() is nowhere to be seen. If that bothers you, you can read more about volumeMeter in Appendix A. Speaking briefly you can assume that it is a function that provides current ambient volume and is designed to simulate short- and long-running tasks in controlled manner.

I have mentioned already that I will attempt to visualise the execution of each scenario. Let’s have a look at first diagram, derived directly from the execution of the above code.

Synchronous execution with constant delay and single volume measurement

Synchronous execution with constant delay and single volume measurement

Note: Diagram convention is explained in Appendix B

Let’s get back to our example.

Looking at the second row of the chart it is clear that the volume measurement happened first, followed by predefined number of beats. Compare the first and third row and the frequency looks spot on. The actual sequence took longer that the simulated one only because the simulated one does not take into consideration time taken to measure the volume.

So, it looks like we are sorted!

Ohh… one moment, I am receiving a phone call from a product owner.

Synchronous scenario with volume measurement before each beat

I have been asked to provide a solution to slightly different problem now. I am assured the change would be minimal (yeah … right). Basically, the single, initial volume measurement will not work well, even in typical scenario. The metronome is started right before the musician plays. That means the measurement is done while it is quiet and then is drowned by the music.

Should be simple, right? Let’s look at the solution to this new set of requirements (comments removed for brevity)

func(bpm param.Bpm, performer metronome.BeatPerformer) {  
   for beatCount := 0; beatCount < numberOfBeats; beatCount ++ {  
      volume := volumeMeter()  
      performer(beatCount, volume)  
      time.Sleep(bpm.Interval())  
   }  
}

It was just a matter of moving volume measurement into the loop, right before the beat is performed.

Let’s look at the timeline:

Synchronous execution with constant delay

Synchronous execution with constant delay

Oh dear, the actual execution time has stretched.

Of course! The call to volumeMeter takes some time, so I cannot expect that fixed delay will produce correct frequency of beats.

We need to anticipate the delay caused by volumeMeter, even though it is out of our control (in real life, because here we are simulating it).

Synchronous scenario with adaptive delay

Optimistic scenario

Looks like a fairly simple exercise. Here is the code:

func(bpm param.Bpm, performer metronome.BeatPerformer) {  
   for beatCount := 0; beatCount < numberOfBeats; beatCount ++ {  
      //let's find out the duration of volume measurement...  
      start := time.Now()  

      volume := volumeMeter()  
      
      performer(beatCount, volume)  
  
      //...so that we can adapt the planned delay accordingly (or skip it if took too long) 
      if adaptiveDelay := bpm.Interval() - time.Since(start); adaptiveDelay > 0 {  
        time.Sleep(adaptiveDelay)  
      }  
   }  
}

I have used one of the most basic benchmarking methods there is in Go:

   start := time.Now()
   //"do stuff" you want to time
   fmt.Println(time.Since(start))

it will print elapsed time (read: time it took to “do stuff”) with nanosecond precision.

3.314µs

Process finished with exit code 0

Function time.Since will return the time.Duration type which ships with a handful of methods to make conversion, rounding, and truncating easy. We see a nice unit symbol because time.Duration.String() method is delivering the appropriate textual representation of the duration value.

Java analogy: the simplest drop-in replacement would be System.nanoTime() which gives means to obtain reading from most precise system timer. When it comes to accurate measurement of elapsed time in Java I would use this construct. However, it returns primitive long so does not ship with any tools to manipulate or present the result nicely though. We could also use System.currentTimeMillis() for rough, low resolution measurement that does not have to be exactly right. Use it with caution for short time-spans. Don’t use it for critical parts.
As it is expected, there is plethora of other classes that let you solve similar problem, whether in frameworks (StopWatch in Spring, Guava and Apache Commons) or core Java (java.time.Instant.now() from Java 8 onwards, java.util.Date.getTime() most of other methods in this class is deprecated in favour of new Java 8 Date Time API).

And the chart will verify our presumptions:

Synchronous execution with adaptive delay

Synchronous execution with adaptive delay

Marvellous! All looks good.

Wait… I suddenly remembered that our hardware specialist once told me, that this volume sampler (that I am testing with) is the best in its class. The majority of devices on the market are not performing too well. Let’s plug a different model in, to see if that’s the case.

I always valued conclusions drawn from an analysis on a base sample of 2.

Note: To prove the case, the volumeMeter is now going to be using TriangularPatternDuration so that the execution time of volumeMeter() will keep changing in a predictable manner between short and overrunning. You can see how second row of the diagram will illustrate changing execution time of volume measurement.

Oscillating execution time of volume sampler

And the results are in.

Synchronous execution with adaptive delay - oscillating execution time

Synchronous execution with adaptive delay - oscillating execution time

Oh no!!! This isn’t going to please anybody. Metronome that can’t keep up? Rubbish. The beat timing (green markers) is inconsistent. If you don’t see it, look at the vertical lines of ghost beats which project the ideal timing onto time axis.

Our solution needs rework.

I’ve read something about the Ticker and how superior it is to provide timing at a cost of few keystrokes.

Thumbnail