liveBook · Manning

Data race

As explained in Learn Go with Pocket-Sized Projects

A data race is a concurrency defect in which multiple execution contexts access the same memory concurrently and at least one access modifies it without adequate synchronization.

A data race occurs when concurrent execution contexts access the same memory and at least one access writes without adequate synchronization. Two simultaneous writes to one cache entry are a race, even when tests sometimes pass. section 7.3.2 section 9.6.1

Detect and correct races with a practical workflow:

  • Run independent subtests with t.Parallel() to exercise concurrent execution. section 7.3.2
  • Run go test -race . to ask Go’s race detector to identify conflicting accesses and show their stack traces. section 7.3.3
  • Protect shared state with a mutex placed beside the data it protects, and defer Unlock() immediately after Lock(). section 7.3.4 section 7.6
  • Use RWMutex only when read concurrency and measured performance justify its additional complexity; a clean race-detector run is not proof that every race is absent. section 7.3.4 section 7.6

Parallel cache writes

Create one cache, then run two parallel subtests that both call Upsert(6, ...) with different values such as "six" and "kuus". A normal test run may appear successful; go test -trimpath -race . reports the conflicting writes and shows that the cache is not thread safe. section 7.3.2 section 7.3.3

> go test --trimpath -race .
==================
WARNING: DATA RACE
Write at 0x00c0000a53e0 by goroutine 13:
  runtime.mapassign_fast64()
      runtime/map_fast64.go:93 +0x0
  learngo-pockets/genericcache.(*Cache[...]).Upsert()
      learngo-pockets/genericcache/cache.go:28 +0x124
  learngo-pockets/genericcache_test.TestCache_Parallel.func1()
      learngo-pockets/genericcache/cache_test.go:73 +0x97
  learngo-pockets/genericcache_test.TestCache_Parallel.func2()
      learngo-pockets/genericcache/cache_test.go:74 +0x47

Previous write at 0x00c0000a53e0 by goroutine 20:
  runtime.mapassign_fast64()
      runtime/map_fast64.go:93 +0x0
  learngo-pockets/genericcache.(*Cache[...]).Upsert()
      learngo-pockets/genericcache/cache.go:28 +0x124
  learngo-pockets/genericcache_test.TestCache_Parallel.func1()
      learngo-pockets/genericcache/cache_test.go:73 +0x97
  learngo-pockets/genericcache_test.TestCache_Parallel.func2()
      learngo-pockets/genericcache/cache_test.go:74 +0x47

Inspect a representative race-detector report showing two goroutines writing through Cache.Upsert at the same map location. section 7.3.3

Listing 7.11 cache.go: Caching with a Mutex
type Cache[K comparable, V any] struct {
    mu   sync.Mutex
    data map[K]V
}

Inspect the cache structure with a mutex stored next to its protected data map. section 7.3.4

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Defines conflicting concurrent access, demonstrates a race report, and applies mutexes; it stops short of proving undetected races absent.
Examples
Uses parallel cache writes and concurrently explored maze pixels to demonstrate conflicting access and undefined behavior.
What you can do after
Diagnose/debug with go test -race, then protect shared cache state with mutexes and deferred unlocks.
How it connects
Connects data races with goroutines, parallel tests, shared variables, mutexes, and read-write mutexes.

Also explained in

Compare how 4 books explain data race →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Go with Pocket-Sized Projects. The section links point to the parts of the book it draws on.