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:
- Load: Atomically loads and returns the value stored at the specified address
- Store: Atomically stores a value at the specified address
- Add: Atomically adds a value to the value stored at the specified address
- Swap: Atomically swaps the value stored at the specified address with a new value
- 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.