Golang: Concurrency: Monitors and Mutexes, A (light) Survey

dm03514
dm03514
May 22, 2019 · 6 min read
Image for post
Image for post

Channel based synchronization vs mutex based synchronization is a polarizing topic within the go community. This post discusses a pattern named “monitor goroutine” which guards access to memory, or an operation, and exposes a channel as an interface. This post explains what a monitor goroutines are and scenarios where this pattern can be useful. Since monitor goroutines are built on channels they are a compelling choice for engineers that are new to concurrent programming. This post then compares monitor goroutines to mutexes and explores some situations where one approach may be more favorable than the other.

Guarding Access Through Monitors

func NewCounterMonitor(ctx context.Context) chan <- int {
ch := make(chan int)

go func() {
counter := 0
for {
select {
case i, ok := <-ch:
if !ok {
return
}
counter += i
case <-ctx.Done():
fmt.Printf("final_count: %d\n", counter)
return
}
}
}()

return ch
}

(There are a couple of useful patterns to make testing monitor routines deterministic but for right now we’ll keep it as an async operation.) . We’ll create a “test” just to drive these examples for this post:

func TestNewCounterMonitor(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
in := NewCounterMonitor(ctx)
in <- 1
}

And to see it in action:

$ go test ./... -v
=== RUN TestNewCounterMonitor
--- PASS: TestNewCounterMonitor (0.00s)
PASS
final_count: 1
ok github.com/dm03514/grokking-go/monitor-goroutines 1.093s

There’s a lot going on in the NewCounterMonitor routine which is one of the reasons channels are so controversial. The memory that is “monitored” (or guarded) is

counter := 0

The rest of the code is boiler plate around setting up the interface (the channel):

ch := make(chan int)
...
case i, ok := <-ch:
...
return ch

kicking off the goroutine, and consuming the channel:

go func() {
for {
select {
case i, ok := <-ch:
}
}
}()

and ensuring that the monitor completes:

case <-ctx.Done():

And when the channel is closed:

i, ok := <-ch

The interface that is used into the monitor is exposed through a channel. A client can send only access the counter through a channel. Go ensures that channels are synchronized and thread-safe by default.

Guarding Access Through Mutexes

type SafeCounter struct {
mu *sync.Mutex
count int
}

func (s *SafeCounter) Inc() {
s.mu.Lock()
defer s.mu.Unlock()
s.count++
}

This example encapsulates the synchronization (through mutex mu) and the operation that needs to be guarded (incrementing count) in a single data-structure to help protect the end user. Mutexes differ from the monitor pattern in that instead of just a channel being shared the full data structure is being shared and guarded with a mutex. The interface to the counter IS the full data structure. This means that in order to use a SafeCounter a reference to the safe counter is passed to each goroutine:

sc := &SafeCounter{
mu: &sync.Mutex{},
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
sc.Inc()
}()
}
wg.Wait()
// sc.count == 10

Topology

Image for post
Image for post
Monitor Goroutine Topology

Monitors provide strict isolation of the counter at the level of a goroutine. This means that only a single goroutine (the monitor) has access to the counter variable. This significantly minimizes the chances of concurrent synchronization errors around accessing it.

Consider the mutex which shares a reference to memory:

Image for post
Image for post
Mutex Based Counter

The Mutex requires sharing a much larger segment of memory (compared to only a channel). This means that the mutex, by default, exposes shared mutable state which is one of the core challenges with concurrent programming. The above examples illustrate how go channels achieve sharing memory by communication, vs how mutexes share memory.

Sequence

Image for post
Image for post

Since the mutex is shared memory it can trivially implement a synchronous (request/response|dispatch/return) API (as seen above).

Performance

Image for post
Image for post

This specific test does not exercise contention among the goroutines. I’m assuming that the mutex performance is due, at least in part, to not having to context switch between goroutines. Though out of the scope of this post, pprof could be used to find out where the source of latency of each approach).

When To Use

async/sync

Mutexes can trivially support synchronous call/response style apis. I have found that much of the benefit of channel safety is lost when trying to orchestrate complex flows

buffering/timing/thresholds

func NewCounterMonitor(ctx context.Context) chan <- int {
ch := make(chan int)

go func() {
counter := 0
ticker := time.NewTicker(10 * time.Second
defer ticker.Stop()
for {
select {
case i, ok := <-ch:
if !ok {
return
}
counter += i
case <-ticker.C:
log.Printf("NewCounterMonitor.counter: %d\n", counter)
case <-ctx.Done():
fmt.Printf("final_count: %d\n", counter)
return
}
}
}()

return ch
}

Since the monitor is the owner of counter it can be accessed anywhere in the loop without being concerned about thread-safety. Imagine adding ticker logic to the mutex based count: another goroutine would have to be spawned which would complicate the implementation.

Correctness over Performance

Pipelines

Conclusion

References

Dm03514 Tech Blog

Dm03514’s Tech Blog

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store