Atomic Operations Fundamentals

Go’s sync/atomic package provides low-level atomic memory operations that form the building blocks for lock-free programming. These operations guarantee that complex manipulations of memory happen indivisibly, without interruption from other goroutines.

Basic Atomic Operations

The sync/atomic package provides several fundamental operations:

  1. Load: Atomically loads and returns the value stored at the specified address
  2. Store: Atomically stores a value at the specified address
  3. Add: Atomically adds a value to the value stored at the specified address
  4. Swap: Atomically swaps the value stored at the specified address with a new value
  5. CompareAndSwap: Atomically compares the value at the specified address with an expected value and, if they match, swaps it with a new value

Here’s a demonstration of these basic operations:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func basicAtomicOperations() {
	// Atomic integer operations
	var counter int64
	
	// Store
	atomic.StoreInt64(&counter, 42)
	fmt.Printf("After Store: %d\n", counter)
	
	// Load
	value := atomic.LoadInt64(&counter)
	fmt.Printf("Load result: %d\n", value)
	
	// Add (returns new value)
	newValue := atomic.AddInt64(&counter, 10)
	fmt.Printf("After Add: counter = %d, returned = %d\n", counter, newValue)
	
	// Swap (returns old value)
	oldValue := atomic.SwapInt64(&counter, 100)
	fmt.Printf("After Swap: counter = %d, old value = %d\n", counter, oldValue)
	
	// CompareAndSwap (returns success boolean)
	swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
	fmt.Printf("CAS with 100->200: counter = %d, swapped = %v\n", counter, swapped)
	
	// Failed CompareAndSwap
	swapped = atomic.CompareAndSwapInt64(&counter, 100, 300)
	fmt.Printf("CAS with 100->300: counter = %d, swapped = %v\n", counter, swapped)
}

func main() {
	basicAtomicOperations()
	
	// Demonstrate concurrent counter
	concurrentCounter()
}

func concurrentCounter() {
	var counter int64
	var wg sync.WaitGroup
	
	// Launch 1000 goroutines that each increment the counter 1000 times
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				atomic.AddInt64(&counter, 1)
			}
		}()
	}
	
	wg.Wait()
	fmt.Printf("Final counter value: %d\n", counter)
	// Expected output: Final counter value: 1000000
}

This example demonstrates the basic atomic operations and shows how they can be used to implement a thread-safe counter without locks. The final value of the counter will always be 1,000,000, demonstrating that the atomic operations correctly handle concurrent access.

Atomic Pointer Operations

The sync/atomic package also provides atomic operations for pointers:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"unsafe"
)

func atomicPointerOperations() {
	type Data struct {
		Value int
		Text  string
	}
	
	// Create initial data
	initialData := &Data{Value: 100, Text: "Initial"}
	
	// Create an atomic pointer
	var dataPtr atomic.Pointer[Data]
	
	// Store initial value
	dataPtr.Store(initialData)
	
	// Load and access
	data := dataPtr.Load()
	fmt.Printf("Initial data: %+v\n", data)
	
	// Atomic pointer swap
	newData := &Data{Value: 200, Text: "Updated"}
	oldData := dataPtr.Swap(newData)
	fmt.Printf("After swap - old: %+v, current: %+v\n", oldData, dataPtr.Load())
	
	// CompareAndSwap
	newerData := &Data{Value: 300, Text: "Newer"}
	swapped := dataPtr.CompareAndSwap(newData, newerData)
	fmt.Printf("CAS result: %v, current: %+v\n", swapped, dataPtr.Load())
	
	// Failed CompareAndSwap
	failedData := &Data{Value: 400, Text: "Failed"}
	swapped = dataPtr.CompareAndSwap(newData, failedData) // Will fail because current is newerData
	fmt.Printf("Failed CAS result: %v, current: %+v\n", swapped, dataPtr.Load())
}

func main() {
	atomicPointerOperations()
	
	// Demonstrate concurrent pointer updates
	concurrentPointerUpdates()
}

func concurrentPointerUpdates() {
	type Config struct {
		Settings map[string]string
		Version  int
	}
	
	// Create initial config
	initialConfig := &Config{
		Settings: map[string]string{"timeout": "30s"},
		Version:  1,
	}
	
	var configPtr atomic.Pointer[Config]
	configPtr.Store(initialConfig)
	
	var wg sync.WaitGroup
	
	// Goroutine that periodically updates the config
	wg.Add(1)
	go func() {
		defer wg.Done()
		
		for i := 0; i < 5; i++ {
			// Get current config
			current := configPtr.Load()
			
			// Create new config based on current
			newConfig := &Config{
				Settings: make(map[string]string),
				Version:  current.Version + 1,
			}
			
			// Copy and update settings
			for k, v := range current.Settings {
				newConfig.Settings[k] = v
			}
			newConfig.Settings["timeout"] = fmt.Sprintf("%ds", (i+2)*10)
			
			// Atomically update the config
			configPtr.Store(newConfig)
			
			// Simulate some work
			for j := 0; j < 1000000; j++ {
				// Busy work
			}
		}
	}()
	
	// Multiple goroutines reading the config
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			
			for j := 0; j < 10; j++ {
				// Read current config atomically
				config := configPtr.Load()
				fmt.Printf("Reader %d: config version %d, timeout %s\n", 
					id, config.Version, config.Settings["timeout"])
				
				// Simulate some work
				for k := 0; k < 500000; k++ {
					// Busy work
				}
			}
		}(i)
	}
	
	wg.Wait()
	
	// Final config
	finalConfig := configPtr.Load()
	fmt.Printf("Final config: version %d, timeout %s\n", 
		finalConfig.Version, finalConfig.Settings["timeout"])
}

This example demonstrates atomic pointer operations, including a practical example of a thread-safe configuration that can be updated atomically without locks. Multiple reader goroutines can access the configuration while it’s being updated by a writer goroutine, without any race conditions.

Atomic Value Type

The atomic.Value type provides a way to atomically load and store values of any type:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func atomicValueDemo() {
	type Config struct {
		Endpoints []string
		Timeout   time.Duration
		MaxRetries int
	}
	
	// Create atomic value
	var configValue atomic.Value
	
	// Store initial config
	initialConfig := Config{
		Endpoints: []string{"http://api.example.com"},
		Timeout:   5 * time.Second,
		MaxRetries: 3,
	}
	configValue.Store(initialConfig)
	
	// Simulate config updates and reads
	var wg sync.WaitGroup
	
	// Config updater
	wg.Add(1)
	go func() {
		defer wg.Done()
		
		for i := 0; i < 5; i++ {
			// Get current config
			currentConfig := configValue.Load().(Config)
			
			// Create updated config
			newConfig := Config{
				Endpoints: append([]string{}, currentConfig.Endpoints...),
				Timeout:   currentConfig.Timeout + time.Second,
				MaxRetries: currentConfig.MaxRetries + 1,
			}
			
			// Add a new endpoint
			newEndpoint := fmt.Sprintf("http://api%d.example.com", i+2)
			newConfig.Endpoints = append(newConfig.Endpoints, newEndpoint)
			
			// Store updated config atomically
			configValue.Store(newConfig)
			fmt.Printf("Updated config: timeout=%v, endpoints=%v, retries=%d\n", 
				newConfig.Timeout, newConfig.Endpoints, newConfig.MaxRetries)
			
			// Simulate work
			time.Sleep(10 * time.Millisecond)
		}
	}()
	
	// Config readers
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			
			for j := 0; j < 10; j++ {
				// Load config atomically
				config := configValue.Load().(Config)
				fmt.Printf("Reader %d: timeout=%v, endpoints=%d, retries=%d\n", 
					id, config.Timeout, len(config.Endpoints), config.MaxRetries)
				
				// Simulate work
				time.Sleep(5 * time.Millisecond)
			}
		}(i)
	}
	
	wg.Wait()
}

func main() {
	atomicValueDemo()
}

This example demonstrates how to use atomic.Value to store and load complex data structures atomically. It’s particularly useful for configuration objects that need to be updated and read concurrently.