1. go
  2. /concurrency
  3. /atomic

Understanding Atomic Operations in Go Programming

Atomic operations provide low-level synchronization primitives that ensure operations are performed atomically, without interruption from other goroutines. This guide covers everything you need to know about using atomic operations effectively.

Atomic Basics

Basic Atomic Types

// Atomic integers
var counter atomic.Int64
counter.Add(1)
value := counter.Load()

// Atomic unsigned integers
var flags atomic.Uint32
flags.Store(1)
current := flags.Load()

// Atomic pointers
var ptr atomic.Pointer[string]
ptr.Store(new(string))
value := ptr.Load()

Atomic Operations

// Add and Subtract
var counter atomic.Int64
counter.Add(10)      // Add 10
counter.Add(-5)      // Subtract 5

// Compare and Swap
var value atomic.Int32
old := value.Load()
swapped := value.CompareAndSwap(old, old+1)

// Swap
var ptr atomic.Pointer[*User]
oldUser := ptr.Swap(newUser)

// Store and Load
var flag atomic.Bool
flag.Store(true)
isSet := flag.Load()

Best Practices

1. Memory Ordering

// Ensure proper memory ordering
type Counter struct {
    value atomic.Int64
    // Other fields that don't need atomic access
    name string
}

func (c *Counter) Increment() int64 {
    return c.value.Add(1)
}

func (c *Counter) Load() int64 {
    return c.value.Load()
}

// Usage
counter := &Counter{name: "example"}
counter.Increment()
value := counter.Load()

2. Performance Considerations

// Use atomic operations for simple values
var atomicCounter atomic.Int64

// Use mutex for complex operations
type ComplexCounter struct {
    mu      sync.Mutex
    counter int64
    history []int64
}

func (c *ComplexCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.counter++
    c.history = append(c.history, c.counter)
}

3. Zero Values

// Atomic types are automatically initialized
var counter atomic.Int64  // Initialized to 0

// No explicit initialization needed
func NewCounter() *atomic.Int64 {
    return new(atomic.Int64)  // Ready to use
}

// Can still initialize explicitly if needed
func NewCounterWithValue(initial int64) *atomic.Int64 {
    counter := new(atomic.Int64)
    counter.Store(initial)
    return counter
}

Common Patterns

1. Flag Management

type Service struct {
    running atomic.Bool
    done    chan struct{}
}

func (s *Service) Start() {
    if s.running.CompareAndSwap(false, true) {
        s.done = make(chan struct{})
        go s.run()
    }
}

func (s *Service) Stop() {
    if s.running.CompareAndSwap(true, false) {
        close(s.done)
    }
}

func (s *Service) run() {
    for {
        select {
        case <-s.done:
            return
        default:
            if !s.running.Load() {
                return
            }
            // Do work
        }
    }
}

2. Reference Counting

type RefCounted struct {
    refs atomic.Int32
    data []byte
}

func (rc *RefCounted) Acquire() {
    rc.refs.Add(1)
}

func (rc *RefCounted) Release() bool {
    return rc.refs.Add(-1) == 0
}

// Usage
obj := &RefCounted{data: make([]byte, 1024)}
obj.Acquire()
// Use object
if obj.Release() {
    // Last reference, clean up
}

3. Double-Checked Locking

type Singleton struct {
    initialized atomic.Bool
    instance    atomic.Pointer[data]
    mu          sync.Mutex
}

func (s *Singleton) getInstance() *data {
    if !s.initialized.Load() {
        s.mu.Lock()
        defer s.mu.Unlock()
        
        if !s.initialized.Load() {
            instance := &data{}
            s.instance.Store(instance)
            s.initialized.Store(true)
        }
    }
    return s.instance.Load()
}

Performance Considerations

1. Atomic vs Mutex

// Atomic: Fast for single operations
var atomicCounter atomic.Int64

func incrementAtomic() {
    atomicCounter.Add(1)
}

// Mutex: Better for multiple operations
type MutexCounter struct {
    mu      sync.Mutex
    counter int64
}

func (c *MutexCounter) increment() {
    c.mu.Lock()
    c.counter++
    c.mu.Unlock()
}

2. Memory Alignment

// Good: Properly aligned atomic values
type Aligned struct {
    // 64-bit atomic values should be 64-bit aligned
    counter atomic.Int64
    flag    atomic.Bool  // Smaller values can follow
}

// Bad: Potential alignment issues
type Unaligned struct {
    flag    atomic.Bool      // 1 byte
    counter atomic.Int64     // Might not be 8-byte aligned
}

Common Mistakes

1. Partial Operations

// Wrong: Non-atomic compound operations
var counter atomic.Int64

func wrongIncrement() int64 {
    current := counter.Load()
    counter.Store(current + 1)  // Race condition
    return current + 1
}

// Right: Atomic operation
func correctIncrement() int64 {
    return counter.Add(1)
}

2. Mixing Atomic and Non-Atomic

// Wrong: Mixed access
type Wrong struct {
    counter int64  // Non-atomic
}

func (w *Wrong) increment() {
    atomic.AddInt64(&w.counter, 1)  // Mixed with atomic
}

// Right: Consistent atomic access
type Right struct {
    counter atomic.Int64
}

func (r *Right) increment() {
    r.counter.Add(1)
}

3. Value Copying

// Wrong: Copying atomic values
type WrongCounter struct {
    value atomic.Int64
}

func (c WrongCounter) increment() {  // Value receiver
    c.value.Add(1)  // Operates on copy
}

// Right: Use pointer receiver
type RightCounter struct {
    value atomic.Int64
}

func (c *RightCounter) increment() {  // Pointer receiver
    c.value.Add(1)  // Operates on original
}

Next Steps

  1. Learn about mutexes
  2. Explore channels
  3. Study memory model
  4. Practice with synchronization

Additional Resources