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
- Learn about mutexes
- Explore channels
- Study memory model
- Practice with synchronization