Mutex and data races
The kitchen has a storeroom, and on its door hangs a sheet with the number of flour sacks. A cook who brings a sack reads the number, adds one and writes the new number down. What happens if two cooks read the sheet at the same moment? Both see 41, both write 42, and one sack drops out of the count.
Goroutines fall into the same trap. A thousand goroutines increment one variable, each by one, and main waits for all of them:
package main
import "fmt"
import "sync"
func main() {
var wg sync.WaitGroup
n := 0
for range 1000 {
wg.Go(func() { n++ })
}
wg.Wait()
fmt.Println(n)
}You would expect 1000, but on a computer with several cores the program usually prints less, for example 970. Each run gives a different number, and on a single core the bug may never show up.
Data race
The operation n++ has three steps:
- Read
n. - Add one.
- Write the result.
Two goroutines may read 41 at the same time and both write 42, and one increment is lost.
When two goroutines access one variable at the same time and at least one of them writes, this is a data race. The result of such a program is unpredictable.
Finding a race
The -race flag helps find a race. The command go run -race main.go prints WARNING: DATA RACE and the lines where the goroutines met.
A race in a map
With a map, a race ends even worse. If several goroutines write to one map without protection, here is what happens:
- the runtime often notices and stops the program with
fatal error: concurrent map writes; - this is not a panic:
recovercannot catch it.
Interviewers like to ask about this: a plain map in Go is not safe for concurrent writes.
sync.Mutex
The kitchen's fix is a single key to the storeroom. Whoever holds the key goes in and updates the sheet, and the others wait at the door. The sync.Mutex type from the sync package is that key: it lets one goroutine at a time reach the data. It has two methods:
Locktakes the key; a goroutine that callsLockon a lockedMutexwaits until the owner callsUnlock;Unlockgives the key back.
var wg sync.WaitGroup
var mu sync.Mutex
n := 0
for range 1000 {
wg.Go(func() {
mu.Lock()
n++
mu.Unlock()
})
}
wg.Wait()
fmt.Println(n) // 1000Let's take the goroutine's body apart:
mu.Lock()takes the key or waits until it is given back.n++does all three steps while the goroutine holds the key.mu.Unlock()gives the key to the next goroutine.
No other goroutine can slip in between the read and the write, so no increment is lost, and the program prints 1000 on every run.
Zero value and copying
A Mutex is declared and passed by the same rules as a WaitGroup:
- the zero value of a
Mutexis unlocked and ready to use; - the literal
sync.Mutex{}gives the same value, sovar wg, mu = sync.WaitGroup{}, sync.Mutex{}declares both variables ready to use; - a
Mutexmust not be copied; - a struct with a
sync.Mutexfield is passed by pointer, and its methods are declared with a pointer receiver.
Rules for a Mutex
This is where the key picture stops working. A real door will not open without the key, but a Mutex holds nobody back by force: a goroutine that never called Lock can still reach n. Hence three rules:
- every goroutine that touches the shared data must call
Lock; - reading a shared variable also needs the
Mutexif other goroutines write to it; - keep as little code as possible between
LockandUnlock, because the other goroutines wait all that time.
Common mistake
A function called Lock but returned on one of its paths without calling Unlock:
var mu sync.Mutex
var stock = 5
func take(n int) bool {
mu.Lock()
if stock < n {
return false // ✗
}
stock -= n
mu.Unlock()
return true
}Here is what happens:
- After the call
take(10)theMutexstays locked: the cook walked off with the key. - The next call to
takewaits onLockforever. - If every goroutine waits like this, the program stops with
fatal error: all goroutines are asleep - deadlock!.
The fix is defer mu.Unlock() right after mu.Lock(): then the Mutex is unlocked however the function exits.
Key points
- A data race is concurrent access to a variable where at least one goroutine writes; the
-raceflag finds it. - Concurrent writes to an unprotected
mapcan stop the program withfatal error: concurrent map writes. mu.Lock()andmu.Unlock()let one goroutine at a time reach the data; everyone who touches the data callsLock.defer mu.Unlock()right afterLockunlocks theMutexon any exit; aMutexis not copied.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.