I'm trying to identify or understand an appropriate technique, idiom, whatever for a specific concurrent programming problem I'm having.
For simplicity's sake, assume I have a real-time graphical user interface (UI) that is redrawn on screen at 10Hz always and forever.
I would like to display a "Busy" indicator on this UI whenever at least one instance of a group of different threads are running, and I want that indicator to stop displaying when precisely 0 of these threads are running. These threads could feasibly be started and stopped at any time as long as the UI is up.
I'm currently implementing this in golang (with relevant snippets further below). But in general, I'm solving this as follows:
Keep R+W access to a counter int waitCount
(number of threads requesting we indicate "Busy") protected via mutex waitLock
.
function drawStatus()
: Redraw the entire UI (occurs every 100ms):
waitLock
waitCount
> 0:waitLock
function startWait()
: When a thread needs to indicate busy:
waitLock
waitCount
waitLock
function stopWait()
: When a thread no longer needs to indicate busy:
waitLock
waitCount
waitLock
To me, it feels like I'm not taking full advantage of golang's concurrency facilities and resorting to the mutexes I'm familiar with. But even still, there is a bug in this code wherein the "Busy" indicator gets dismissed prematurely.
I'm honestly not looking for anyone to help identify that bug, but rather I'm trying to convey the specific logic I'm interested in. Is there a more idiomatic golang way to approach this problem? Or is there a more general programming pattern that I should investigate? Does this technique I'm using have any particular name? And suggestions or pointers on doing this properly would be great. Thanks.
And here's some doctor'd up snippets that implement the above logic
var WaitCycle = [...]rune{'
Since all you're doing is locking on a single counter, you could simplify and just use the sync/atomic package. Call AddInt32(&x, 1)
when starting a goroutine, and AddInt32(&x, -1)
when ending it. Call LoadInt32(&x)
from your drawing goroutine.
It is use case dependent (and you may choose what you wish, and no body cares until you produce a bug or hit the performance penalty), Channels hide Lock inside and make the coding simpler with a little performance cost - so I recommend using channels for the general use cases unless you are considering more performance):
Use channels if:
1 - transferring ownership
2 - coordinating
Use primitives if:
3 - performance critical
4 - Guarding internal state of struct
ref: page 33
Since you are using soft real time UI coordinating the number of goroutines, and not performance critical code, I recommend using channels, I simplified your code here in this example:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 100; i++ {
go job() // e.g.: run all jobs
}
busy := 0
time.Sleep(10 * time.Millisecond) // or make sure at least on goroutine started
// 10Hz:
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
select {
case n := <-ch:
busy += n
case <-tick.C:
// forces the UI to redraw all changed screen regions
fmt.Printf(" %d ", busy)
if busy == 0 {
return
}
}
}
}
func job() {
ch <- +1
time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
ch <- -1
}
var ch = make(chan int, 1)