1. go
  2. /concurrency
  3. /memory-model

Go Memory Model

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

Memory Model Basics

Happens Before

The key concept in Go's memory model is "happens before", which defines when the effects of one goroutine are guaranteed to be visible to another goroutine. This relationship is the fundamental building block of synchronization in Go.

var a, b int

func f() {
    a = 1 // A
    b = 2 // B
}

func g() {
    print(b) // C
    print(a) // D
}

Without synchronization, there's no guarantee about the order in which these operations will be observed by different goroutines.

Initialization Guarantee

Go provides important guarantees about package initialization:

var a = 1 // Package level variable

func init() {
    a = 2 // Guaranteed to happen before main starts
}

func main() {
    print(a) // Always prints 2
}

Synchronization Mechanisms

Channel Communication

Channel operations in Go provide strong synchronization guarantees:

var done = make(chan bool)
var msg string

func setup() {
    msg = "hello"
    done <- true // Send guarantees msg is visible to receiver
}

func main() {
    go setup()
    <-done        // Receive guarantees msg is visible here
    print(msg)    // Guaranteed to print "hello"
}

Mutex Synchronization

Mutex provides mutual exclusion and memory synchronization:

var mu sync.Mutex
var value int

func modify() {
    mu.Lock()
    value++      // Protected access
    mu.Unlock()
}

func read() int {
    mu.Lock()
    v := value   // Protected access
    mu.Unlock()
    return v
}

Atomic Operations

Atomic operations provide low-level synchronization:

var flag atomic.Bool

func toggle() {
    flag.Store(true)  // Atomic store
}

func check() bool {
    return flag.Load() // Atomic load
}

Common Pitfalls

Data Races

A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write:

var counter int

func race() {
    go func() {
        counter++ // Race condition!
    }()
    counter++ // Race condition!
}

Fix using synchronization:

var counter int
var mu sync.Mutex

func noRace() {
    go func() {
        mu.Lock()
        counter++
        mu.Unlock()
    }()
    mu.Lock()
    counter++
    mu.Unlock()
}

False Sharing

False sharing occurs when variables used by different processors lie on the same cache line:

type Counters struct {
    a uint64
    b uint64
    c uint64
}

Fix by adding padding or using atomic types:

type Counters struct {
    a uint64
    _ [7]uint64 // padding
    b uint64
    _ [7]uint64
    c uint64
}

Best Practices

  1. Always use explicit synchronization when sharing data between goroutines
  2. Prefer channels for communicating between goroutines
  3. Use the race detector during testing and development
  4. Document synchronization requirements in your code
  5. Keep critical sections small when using mutexes

Tools and Debugging

Race Detector

Use the race detector to find synchronization issues:

go test -race ./...
go run -race program.go

Memory Sanitizer

For more thorough memory analysis:

go test -msan ./...

Further Reading

  1. Official Go Memory Model Documentation
  2. Synchronization Primitives
  3. Channel Communication
  4. Race Conditions

Common Questions

  1. Q: Why isn't my concurrent program behaving deterministically? A: Without proper synchronization, concurrent programs can exhibit non-deterministic behavior due to race conditions.

  2. Q: When should I use channels vs. mutexes? A: Use channels for communication between goroutines and mutexes for protecting shared state.

  3. Q: How can I ensure all goroutines see my updates? A: Use appropriate synchronization mechanisms like channels, mutexes, or atomic operations.