Understanding Mutexes in Go Programming
Mutexes (mutual exclusion locks) are synchronization primitives that prevent multiple goroutines from accessing shared resources simultaneously. This guide covers everything you need to know about using mutexes effectively.
Mutex Basics
Basic Mutex Usage
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
// Usage
counter := &Counter{}
for i := 0; i < 1000; i++ {
go counter.Increment()
}
RWMutex Usage
type Store struct {
mu sync.RWMutex
data map[string]string
}
func (s *Store) Get(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.data[key]
}
func (s *Store) Set(key, value string) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
Best Practices
1. Lock Scope
// Good: Minimal lock scope
type Cache struct {
mu sync.Mutex
items map[string]Item
}
func (c *Cache) Get(key string) (Item, bool) {
c.mu.Lock()
item, exists := c.items[key]
c.mu.Unlock()
return item, exists
}
// Bad: Lock held during expensive operation
func (c *Cache) Process(key string) error {
c.mu.Lock()
defer c.mu.Unlock()
item := c.items[key]
// Expensive operation while holding lock
result := expensiveOperation(item)
c.items[key] = result
return nil
}
// Better: Minimize locked section
func (c *Cache) Process(key string) error {
c.mu.Lock()
item := c.items[key]
c.mu.Unlock()
// Expensive operation without holding lock
result := expensiveOperation(item)
c.mu.Lock()
c.items[key] = result
c.mu.Unlock()
return nil
}
2. Consistent Lock Order
// Good: Consistent lock order
type Account struct {
mu sync.Mutex
balance int
}
func Transfer(from, to *Account, amount int) error {
// Always lock in the same order to prevent deadlocks
if from.balance < amount {
return errors.New("insufficient funds")
}
// Lock accounts in order of pointer address
if uintptr(unsafe.Pointer(from)) < uintptr(unsafe.Pointer(to)) {
from.mu.Lock()
to.mu.Lock()
} else {
to.mu.Lock()
from.mu.Lock()
}
defer func() {
from.mu.Unlock()
to.mu.Unlock()
}()
from.balance -= amount
to.balance += amount
return nil
}
3. Composition with Mutex
// Good: Embed mutex
type SafeMap struct {
sync.RWMutex
data map[string]interface{}
}
func (m *SafeMap) Get(key string) interface{} {
m.RLock()
defer m.RUnlock()
return m.data[key]
}
// Bad: Expose mutex
type UnsafeMap struct {
Mutex sync.RWMutex // Exported field
Data map[string]interface{}
}
Common Patterns
1. Double-Checked Locking
type Singleton struct {
once sync.Once
data *Data
}
func (s *Singleton) GetData() *Data {
s.once.Do(func() {
s.data = &Data{}
})
return s.data
}
// Usage
var instance Singleton
data := instance.GetData()
2. Copy-on-Write
type CopyOnWrite struct {
mu sync.RWMutex
data atomic.Value
}
func (c *CopyOnWrite) Update(fn func(map[string]string) map[string]string) {
c.mu.Lock()
defer c.mu.Unlock()
old := c.data.Load().(map[string]string)
new := fn(copyMap(old))
c.data.Store(new)
}
func (c *CopyOnWrite) Get() map[string]string {
return c.data.Load().(map[string]string)
}
func copyMap(m map[string]string) map[string]string {
result := make(map[string]string, len(m))
for k, v := range m {
result[k] = v
}
return result
}
3. Resource Pool
type Pool struct {
mu sync.Mutex
items []interface{}
factory func() interface{}
}
func (p *Pool) Get() interface{} {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.items) == 0 {
return p.factory()
}
item := p.items[len(p.items)-1]
p.items = p.items[:len(p.items)-1]
return item
}
func (p *Pool) Put(item interface{}) {
p.mu.Lock()
defer p.mu.Unlock()
p.items = append(p.items, item)
}
Performance Considerations
1. Lock Contention
// High contention: Single lock
type GlobalCache struct {
mu sync.Mutex
items map[string]interface{}
}
// Lower contention: Sharded locks
type ShardedCache struct {
shards [256]shard
hashFn func(string) uint8
}
type shard struct {
mu sync.Mutex
items map[string]interface{}
}
func (c *ShardedCache) Get(key string) interface{} {
shard := &c.shards[c.hashFn(key)]
shard.mu.Lock()
defer shard.mu.Unlock()
return shard.items[key]
}
2. RWMutex vs Mutex
// Use RWMutex for read-heavy workloads
type ReadHeavy struct {
mu sync.RWMutex
data map[string]string
}
func (r *ReadHeavy) Get(key string) string {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data[key]
}
// Use Mutex for write-heavy workloads
type WriteHeavy struct {
mu sync.Mutex
data map[string]string
}
func (w *WriteHeavy) Update(key, value string) {
w.mu.Lock()
defer w.mu.Unlock()
w.data[key] = value
}
Common Mistakes
1. Copying Mutex
// Wrong: Copying mutex
type Wrong struct {
mu sync.Mutex
}
func (w Wrong) Bad() { // Receiver is a copy
w.mu.Lock() // Locks the copy, not original
defer w.mu.Unlock()
}
// Right: Pointer receiver
func (w *Wrong) Good() {
w.mu.Lock() // Locks the original
defer w.mu.Unlock()
}
2. Recursive Locking
// Wrong: Recursive locking
type Recursive struct {
mu sync.Mutex
}
func (r *Recursive) Bad() {
r.mu.Lock()
r.recursiveCall() // Deadlock!
r.mu.Unlock()
}
func (r *Recursive) recursiveCall() {
r.mu.Lock()
defer r.mu.Unlock()
}
// Right: Redesign to avoid recursive locking
type Better struct {
mu sync.Mutex
}
func (b *Better) Good() {
b.mu.Lock()
defer b.mu.Unlock()
b.nonLockingCall()
}
func (b *Better) nonLockingCall() {
// Work without locking
}
3. Forgetting to Unlock
// Wrong: Missing unlock on error path
func (c *Cache) Wrong(key string) error {
c.mu.Lock()
if _, exists := c.items[key]; !exists {
return errors.New("not found") // Mutex never unlocked!
}
// Process item
c.mu.Unlock()
return nil
}
// Right: Use defer
func (c *Cache) Right(key string) error {
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.items[key]; !exists {
return errors.New("not found")
}
// Process item
return nil
}
Next Steps
- Learn about channels
- Explore goroutines
- Study select
- Practice with atomic operations