#golang#performance#memory-model#concurrency#benchmarking

High-Performance Go: sync/atomic vs sync.Mutex and the Go Memory Model

"Deep benchmark comparison between sync/atomic and sync.Mutex in Go, exploring cache coherence, memory models, and zero-lock pointer swaps."

By huud
0 views
~4 min read
High-Performance Go: sync/atomic vs sync.Mutex and the Go Memory Model

High-Performance Go: sync/atomic vs sync.Mutex and the Go Memory Model

In concurrent programming, race conditions occur when multiple goroutines access shared memory concurrently and at least one access is a write. Go provides two primary tools for thread synchronization at the shared-memory level: mutex locks (sync.Mutex / sync.RWMutex) and atomic CPU primitives (sync/atomic).

Understanding how to choose between locks and atomics requires familiarity with cache coherence, CPU memory barriers, and the official Go Memory Model.


The Go Memory Model: Happens-Before Guarantee

The Go Memory Model defines the exact conditions under which a read of a variable in one goroutine is guaranteed to observe a value produced by a write to the same variable in another goroutine.

Without synchronization, modern superscalar CPUs and Go compilers aggressively reorder read/write instructions for pipelining efficiency.

css
Goroutine A (Core 0)                Goroutine B (Core 1)
--------------------                --------------------
a = 1                               for b == 0 {}
b = 1                               print(a) // MAY PRINT 0 WITHOUT BARRIERS!

Synchronization primitives establish a happens-before edge, creating hardware memory barriers that synchronize L1/L2/L3 cache lines across CPU cores.


sync.Mutex vs sync.atomic Deep-Dive

sync.Mutex: Hybrid OS-Level Synchronization

A Go mutex is a hybrid construct:

  1. Fast Path: It attempts a single atomic.CompareAndSwapInt32 on the mutex state bit. If uncontended, it takes ~5–10 nanoseconds.
  2. Slow Path: If contended, the calling goroutine actively spins briefly on the CPU. If the lock remains held, the goroutine parks on an M-runtime semaphore wait queue, context-switching the OS thread (MM) to run other runnable goroutines (GG).

sync/atomic: Hardware-Level Instruction

Atomic operations map directly to specialized CPU instructions (e.g., LOCK CMPXCHG, LOCK XADD on x86-64, or LDREX/STREX on ARM):

  • No OS context switching.
  • No goroutine descheduling.
  • Single-instruction memory fence synchronization.
  • Takes ~1–3 nanoseconds.

Practical Benchmark Comparison

Consider a global request counter accessed by hundreds of concurrent goroutines:

go
package syncbench_test

import (
	"sync"
	"sync/atomic"
	"testing"
)

type MutexCounter struct {
	mu    sync.Mutex
	count int64
}

func (c *MutexCounter) Inc() {
	c.mu.Lock()
	c.count++
	c.mu.Unlock()
}

func (c *MutexCounter) Load() int64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

type AtomicCounter struct {
	count atomic.Int64
}

func (c *AtomicCounter) Inc() {
	c.count.Add(1)
}

func (c *AtomicCounter) Load() int64 {
	return c.count.Load()
}

func BenchmarkMutexCounter(b *testing.B) {
	var c MutexCounter
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			c.Inc()
		}
	})
}

func BenchmarkAtomicCounter(b *testing.B) {
	var c AtomicCounter
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			c.Inc()
		}
	})
}

Benchmark Results (32 Cores x86-64)

text
BenchmarkMutexCounter-32    18,490,201    62.4 ns/op    0 B/op    0 allocs/op
BenchmarkAtomicCounter-32   94,821,432    12.1 ns/op    0 B/op    0 allocs/op

Atomic operations demonstrate ~5x throughput improvement under extreme multi-core contention for scalar updates.


Lock-Free Data Structures with atomic.Pointer[T] (Go 1.19+)

The atomic.Pointer[T] generic type allows thread-safe atomic pointer swaps without mutex locks. This is ideal for read-heavy configurations that reload periodically in the background (Copy-On-Write):

go
package config

import (
	"sync/atomic"
)

type ServerConfig struct {
	RateLimit   int
	FeatureFlag bool
	Hosts       []string
}

type ConfigStore struct {
	current atomic.Pointer[ServerConfig]
}

func NewStore(initial *ServerConfig) *ConfigStore {
	cs := &ConfigStore{}
	cs.current.Store(initial)
	return cs
}

// GetConfig is zero-lock, zero-wait, non-blocking read
func (cs *ConfigStore) GetConfig() *ServerConfig {
	return cs.current.Load()
}

// Reload creates a new copy and atomically swaps the pointer
func (cs *ConfigStore) Reload(newCfg *ServerConfig) {
	cs.current.Store(newCfg)
}

Memory Alignment & False Sharing Traps

1. 64-bit Word Alignment

On 32-bit platforms, 64-bit atomics require memory alignment to 8-byte boundaries, or the runtime will crash with a SIGSEGV. Using Go 1.19+ types (atomic.Int64, atomic.Uint64) automatically handles struct padding.

2. False Sharing

When two atomic variables reside on the same 64-byte CPU cache line, updates by different CPU cores invalidate the entire cache line, causing cache thrashing:

go
// BAD: False sharing hazard across multiple CPU cores
type Metrics struct {
	ReadOps  atomic.Uint64 // Sits on Cache Line 0
	WriteOps atomic.Uint64 // Sits on same Cache Line 0!
}

// GOOD: Cache padding ensures isolated cache lines
type PaddedMetrics struct {
	ReadOps  atomic.Uint64
	_        [56]byte // Pad to fill 64-byte L1 cache line
	WriteOps atomic.Uint64
	_        [56]byte
}

Decision Matrix

Use Case Recommended Primitive Rationale
Single Counter / Flag / State sync/atomic (atomic.Int64, atomic.Bool) Lowest latency, non-blocking
Read-mostly Config Refresh atomic.Pointer[T] Lock-free Copy-On-Write
Multi-field Struct Invariants sync.Mutex Multiple variables need atomic consistency together
Long-running I/O Section sync.Mutex Atomics cannot span across I/O pauses
Read-heavy, Rare-write Maps sync.RWMutex / sync.Map Shared read lock concurrency

Conclusion

Use sync/atomic for isolated numeric states, metrics counters, and pointer swaps. Use sync.Mutex whenever consistency spans multiple dependent fields. Avoid prematurely optimizing with complex lock-free algorithms unless benchmarks prove mutex contention is your application's primary bottleneck.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.