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

Metronome's Cacophony (12/14) - concurrency in Go - 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.

 1func(bpm param.Bpm, performer metronome.BeatPerformer) {
 2
 3   // channel will carry requests for volume from main loop to the volume measuring Goroutine
 4   // I had to add buffer of one as, straight from start, I need to send two requests without
 5   // receiver attached (which would block)
 6   volumeRequestsChannel := make(chan bool, 1)
 7
 8   // channel will deliver volume measurements from volume measuring Goroutine to the main loop
 9   // no need for buffering as main loop will swiftly receive all messages
10   volumeResponsesChannel := make(chan int)
11
12   // Goroutine which drains volume requests channel, measures
13   // volume on each request, then sends the volume
14   // in response message
15   go func() {
16      for range volumeRequestsChannel {
17         // request arrived
18
19         // blocking volume measurement (but does not impact main timing
20         // loop as this operation runs in Goroutine)
21         volume := volumeMeter()
22
23         // send the latest measurement as response
24         volumeResponsesChannel <- volume
25      }
26      // at this point we know that volume requests channel
27      // is closed and empty (because for-range loop ended)
28
29      // close volume response channel to indicate this Goroutine is ending
30      close(volumeResponsesChannel)
31   }()
32
33   // request first volume measurement, so that we know when to start the timer
34   volumeRequestsChannel <- true
35
36   // request next volume measurement
37   volumeRequestsChannel <- true
38
39   // once we get the first volume measurement the timer can start
40   // without this the time span between the first two beats (only)
41   // would likely be longer than desired interval
42   volume := <-volumeResponsesChannel
43
44   // starting timer right after first volume arrived ensures that the next timing event
45   // will have at least this volume to work with (otherwise it would have to wait for some volume or default it)
46   ticker := time.NewTicker(bpm.Interval())
47   defer ticker.Stop()
48
49   // first beat performed synchronously to avoid complicating the loop's code
50   performer(0, volume)
51
52   // for-loop until we have performed numberOfBeats - 1 (one already done above),
53   // notice this loop does not increment beatCount as the beats don't happen on each iteration
54   // this loop resembles more of a "while loop"
55   for beatCount := 1; beatCount < numberOfBeats; {
56      // non-blocking select which will try to retrieve timing message or volume,
57      // in case none available, it will iterate again immediately, until one of the
58      // messages is available
59      select {
60      case <-ticker.C:
61         // we got timing message back from Ticker, it's time to perform a beat
62         performer(beatCount, volume) 
63         // and increment the count
64         beatCount++
65      case volume = <-volumeResponsesChannel:
66         // fresh volume value arrived, overwrite what's in the volume variable
67         // and request next volume
68         volumeRequestsChannel <- true
69      default:
70      }
71   }
72   //indicate to volume measuring Goroutine that there will be no more requests
73   // that will end for-reach loop in Goroutine
74   close(volumeRequestsChannel)
75
76   //wait for last volume response to be delivered and volume response channel closed
77   for range volumeResponsesChannel {
78   }
79
80   // Goroutine finished, both channels are drained and empty
81   // we can finish the program gracefully now
82}

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
Interactive diagram


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