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{'🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'}
// type Layout holds the high level components of the terminal user interface
type Layout struct {
//
// ... other fields hidden for example ...
//
waitLock sync.Mutex
waitIndex int // the current index of the "busy" rune cycle
waitCount int // the current number of tasks enforcing the "busy" state
}
// function show() starts drawing the user interface.
func (l *Layout) show() *ReturnCode {
// timer forcing a redraw @ 10Hz
go func(l *Layout) {
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
select {
case <-tick.C:
// forces the UI to redraw all changed screen regions
l.ui.QueueUpdateDraw(func() {})
}
}
}(l)
if err := l.ui.Run(); err != nil {
return rcTUIError.specf("show(): ui.Run(): %s", err)
}
return nil
}
// function drawStatus() draws the "Busy" indicator at a specific UI position
func (l *Layout) drawStatus(...) {
l.waitLock.Lock()
if l.waitCount > 0 {
l.waitIndex = (l.waitIndex + 1) % WaitCycleLength
waitRune := fmt.Sprintf(" %c ", WaitCycle[l.waitIndex])
drawToScreen(waitRune, x-1, y, width)
}
l.waitLock.Unlock()
}
// function startWait() safely fires off the "Busy" indicator on the status bar
// by resetting the current index of the status rune cycle and incrementing the
// number of goroutines requesting the "Busy" indicator.
func (l *Layout) startWait() {
l.waitLock.Lock()
if 0 == l.waitCount {
l.waitIndex = 0
}
l.waitCount++
l.waitLock.Unlock()
}
// function stopWait() safely hides the "Busy" indicator on the status bar by
// decrementing the number of goroutines requesting the "Busy" indicator.
func (l *Layout) stopWait() {
l.waitLock.Lock()
l.waitCount--
l.waitLock.Unlock()
}
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.