1. go
  2. /concurrency
  3. /mutexes

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

  1. Learn about channels
  2. Explore goroutines
  3. Study select
  4. Practice with atomic operations

Additional Resources