welcome at SebWalak.com

Metronome's Cacophony (12/14) - concurrency in Go (GoLang) - Solution with Channel

Asynchronous approach (ctd)

Channels (ctd)

Solution with channels and a Goroutine

This solution uses Goroutine to measure volume upon request, and a request and response channels to communicate between Goroutine and main loop. In practise, the volume measurement is continuous, because request for volume is sent as soon as volume (response) is received.

func(bpm param.Bpm, performer metronome.BeatPerformer) {

   // channel will carry requests for volume from main loop to the volume measuring Goroutine
   // I had to add buffer of one as, straight from start, I need to send two requests without
   // receiver attached (which would block)
   volumeRequestsChannel := make(chan bool, 1)

   // channel will deliver volume measurements from volume measuring Goroutine to the main loop
   // no need for buffering as main loop will swiftly receive all messages
   volumeResponsesChannel := make(chan int)

   // Goroutine which drains volume requests channel, measures
   // volume on each request, then sends the volume
   // in response message
   go func() {
      for range volumeRequestsChannel {
         // request arrived

         // blocking volume measurement (but does not impact main timing
         // loop as this operation runs in Goroutine)
         volume := volumeMeter()

         // send the latest measurement as response
         volumeResponsesChannel <- volume
      // at this point we know that volume requests channel
      // is closed and empty (because for-range loop ended)

      // close volume response channel to indicate this Goroutine is ending

   // request first volume measurement, so that we know when to start the timer
   volumeRequestsChannel <- true

   // request next volume measurement
   volumeRequestsChannel <- true

   // once we get the first volume measurement the timer can start
   // without this the time span between the first two beats (only)
   // would likely be longer than desired interval
   volume := <-volumeResponsesChannel

   // starting timer right after first volume arrived ensures that the next timing event
   // will have at least this volume to work with (otherwise it would have to wait for some volume or default it)
   ticker := time.NewTicker(bpm.Interval())
   defer ticker.Stop()

   // first beat performed synchronously to avoid complicating the loop's code
   performer(0, volume)

   // for-loop until we have performed numberOfBeats - 1 (one already done above),
   // notice this loop does not increment beatCount as the beats don't happen on each iteration
   // this loop resembles more of a "while loop"
   for beatCount := 1; beatCount < numberOfBeats; {
      // non-blocking select which will try to retrieve timing message or volume,
      // in case none available, it will iterate again immediately, until one of the
      // messages is available
      select {
      case <-ticker.C:
         // we got timing message back from Ticker, it's time to perform a beat
         performer(beatCount, volume) 
         // and increment the count
      case volume = <-volumeResponsesChannel:
         // fresh volume value arrived, overwrite what's in the volume variable
         // and request next volume
         volumeRequestsChannel <- true
   //indicate to volume measuring Goroutine that there will be no more requests
   // that will end for-reach loop in Goroutine

   //wait for last volume response to be delivered and volume response channel closed
   for range volumeResponsesChannel {

   // Goroutine finished, both channels are drained and empty
   // we can finish the program gracefully now

Instead of detailed walk through the code please look at comments within.

When creating this solution my intention was to make its architecture similar to previous iterations, so that it does not come as a shock.

Looking at the captured execution timing there’s no surprises there:

Asynchronous - channels and Goroutine

Asynchronous - channels and Goroutine