liveBook · Manning

Critical region

As explained in 100 Go Mistakes and How to Avoid Them

A critical section is a region of code that accesses shared state and must be protected from concurrent execution by other operations.

A critical region is the code that must run without another goroutine accessing the same shared state concurrently. In Go, a mutex marks its boundary: lock before the access, then unlock afterward. For two goroutines incrementing one variable, `i++` belongs inside that boundary. Otherwise, both can read the old value and write back the same incremented value. section 8.4.1

Protect the increment

Each goroutine must use the same `sync.Mutex` around `i++`: `mutex.Lock(); i++; mutex.Unlock()`. This protects the read, increment, and write together. For a supported numeric type, `atomic.AddInt64(&i, 1)` is another option. A channel can instead send increment requests to a single goroutine that owns the variable. Choose based on what must be shared: atomic operations cover specific numeric types, while a mutex can protect a larger operation or data structure. section 8.4.1

A common mistake is to lock only long enough to assign a map or slice to a local variable, then use that variable after unlocking. The assignment does not copy the underlying data. If another goroutine mutates it, the supposedly separate work still accesses shared state; a concurrent balance update and average calculation produces a data race detectable with `-race`. section 9.10

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    balances := c.balances                  #1
    c.mu.RUnlock()
 
    sum := 0.
    for _, balance := range balances {      #2
        sum += balance
    }
    return sum / float64(len(balances))
}

The read lock ends before iteration. `balances := c.balances` retains access to the shared map rather than making an independent copy. section 9.10

Set the boundary around the data actually being accessed:

  • For inexpensive iteration, hold `RLock` through the entire average calculation, then release it. This protects every read of the map. section 9.10
  • To calculate outside the lock, allocate a new map and copy its entries while holding `RLock`; only then unlock and iterate over the copy. section 9.10

The lock itself must also be shared. If a struct contains a `sync.Mutex`, a value-receiver method copies that mutex on each call. The method may visibly call `Lock` and `Unlock`, yet concurrent calls no longer enter one shared critical region. A pointer receiver avoids copying the struct; alternatively, store a pointer to the mutex and initialize it before use. `go vet` can flag a method that passes a lock by value. section 9.14

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Works through lock boundaries, aliased maps and slices, and copied mutexes using concrete race failures and fixes.
Examples
Uses a shared increment, a balance-cache average over a map, and a counter whose value receiver copies its mutex.
What it assumes
Assumes familiarity with Go goroutines, methods, maps, and slices; defines the mutex-protected region through examples.
What you can do after
Diagnose/debug with `-race` and `go vet`, then correct lock scope or mutex ownership in concurrent Go code.
How it connects
Connects critical regions to data races, mutexes, atomic operations, channels, map and slice aliasing, and value receivers.

Also explained in

Compare how 9 books explain critical region →

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.