liveBook · Manning

Locking

As explained in 100 Go Mistakes and How to Avoid Them

A mutex is a synchronization primitive that provides mutual exclusion by allowing at most one execution to enter a protected section at a time.

A mutex provides mutual exclusion: at most one goroutine can enter a protected critical section at a time. Use a lock when goroutines must access shared mutable state, not simply because a program uses concurrency. Channels are often a better fit when goroutines need to pass results or coordinate work. section 8.4.1 section 8.3

Protect the entire update

Two goroutines executing `i++` without synchronization can both read 0 and both write back 1. Put the read, increment, and write inside the same lock in each goroutine: var mu sync.Mutex var i int go func() { mu.Lock() i++ mu.Unlock() }() // The other goroutine uses the same mu around its i++. After both updates finish, `i` is 2. For supported numeric types, an atomic operation is another option. Alternatively, send increment requests through a channel and let one goroutine own `i`. section 8.4.1

A lock prevents conflicting access only where the lock is held. In a cache backed by a map, locking while assigning `balances := c.balances` does not make later iteration safe: the new variable still refers to the same map data. A slice assignment similarly retains a shared backing array. section 9.10

Keep protection aligned with the data actually accessed:

  • For a short calculation over a shared map, hold `RLock` through the entire iteration and calculation, then call `RUnlock`. Writers use `Lock` and `Unlock`. section 9.10
  • If the work outside the lock is substantial, copy every map entry while holding `RLock`, unlock, and calculate from the independent copy. Copying only the map variable is insufficient. section 9.10
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    m := make(map[string]float64, len(c.balances))     #1
    for k, v := range c.balances {
        m[k] = v
    }
    c.mu.RUnlock()
 
    sum := 0.
    for _, balance := range m {
        sum += balance
    }
    return sum / float64(len(m))
}

The map-copy implementation shows which work must stay inside the read lock before calculation can safely continue after unlocking. section 9.10

Do not copy the lock

A struct can contain both `mu sync.Mutex` and a shared map. If its `Increment` method has a value receiver, each call copies the struct—and the mutex—while the map data remains shared. The goroutines then lock different mutexes, so `-race` can report a data race. Use a pointer receiver, `func (c *Counter) Increment(name string)`, to keep one shared lock. A pointer-valued mutex field is another option, but it must be initialized before `Lock` is called. `go vet` can flag a method that passes a lock by value. section 9.14

Locking can remove a data race without making every result deterministic. If two properly locked goroutines assign different values to the same variable, the final value can still depend on which assignment runs last. section 8.4.1 section 8.7

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Defines mutual exclusion, traces a lost update, and diagnoses incomplete map locking and copied mutexes.
Examples
Uses competing increments, a balance cache backed by a map, and a counter whose value receiver copies its mutex.
What it assumes
Assumes familiarity with Go goroutines, maps, slices, and methods while defining mutexes and critical sections.
What you can do after
Diagnose/debug with locking by extending protection across shared-data access and checking for inadvertently copied mutexes.
How it connects
Relates mutexes to goroutines, critical sections, channels, atomic operations, data races, and race conditions.

Also explained in

Compare how 18 books explain locking →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.