Introduction
The sync/atomic
package in Go provides low-level atomic memory primitives for managing shared variables without using locks. Atomic operations are useful for implementing lock-free data structures and are often more efficient than using mutexes. In this chapter, you will learn the basics of using atomic variables in Go, including common atomic operations and best practices.
Common Atomic Operations
The sync/atomic
package provides several functions for performing atomic operations on integers and pointers. These operations include adding, loading, storing, and comparing and swapping.
Atomic Add
The AddInt32
and AddInt64
functions perform an atomic addition operation.
Example:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64
atomic.AddInt64(&counter, 1)
fmt.Println(counter) // Output: 1
}
Atomic Load
The LoadInt32
and LoadInt64
functions read the value of an integer atomically.
Example:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64 = 42
value := atomic.LoadInt64(&counter)
fmt.Println(value) // Output: 42
}
Atomic Store
The StoreInt32
and StoreInt64
functions store a value into an integer atomically.
Example:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64
atomic.StoreInt64(&counter, 42)
fmt.Println(counter) // Output: 42
}
Atomic Compare and Swap
The CompareAndSwapInt32
and CompareAndSwapInt64
functions perform an atomic compare-and-swap operation. This operation sets the variable to a new value if it currently holds the expected old value.
Example:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64 = 42
swapped := atomic.CompareAndSwapInt64(&counter, 42, 100)
fmt.Println(swapped) // Output: true
fmt.Println(counter) // Output: 100
}
Atomic Pointer Operations
The sync/atomic
package also provides functions for atomic operations on pointers, such as LoadPointer
, StorePointer
, and CompareAndSwapPointer
.
Example: Atomic Pointer Operations
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
func main() {
var ptr unsafe.Pointer
str := "Hello, World!"
atomic.StorePointer(&ptr, unsafe.Pointer(&str))
loadedPtr := atomic.LoadPointer(&ptr)
fmt.Println(*(*string)(loadedPtr)) // Output: Hello, World!
newStr := "Hello, Go!"
swapped := atomic.CompareAndSwapPointer(&ptr, loadedPtr, unsafe.Pointer(&newStr))
fmt.Println(swapped) // Output: true
fmt.Println(*(*string)(atomic.LoadPointer(&ptr))) // Output: Hello, Go!
}
Using Atomic Variables in Concurrency
Atomic variables are especially useful in concurrent programs where multiple goroutines need to read and write shared variables without the overhead of locks.
Example: Concurrent Counter with Atomic Variables
Example:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter) // Output should be 1000
}
In this example, the counter variable is incremented atomically by multiple goroutines, ensuring that the final value is correct without using locks.
Best Practices
- Use Atomic Operations for Simple Counters: Atomic operations are ideal for simple counters and flags where the overhead of locks is unnecessary.
- Avoid Complex Logic: Atomic operations are low-level primitives and should be used for simple operations. For more complex logic, consider using higher-level synchronization primitives like mutexes.
- Ensure Proper Alignment: Atomic variables should be properly aligned to ensure correct behavior. On most platforms, this means ensuring that the variable’s address is a multiple of its size.
- Use Atomic Types: Prefer using atomic types provided by the
sync/atomic
package for clarity and correctness.
Conclusion
Atomic variables in Go provide an efficient way to manage shared state without using locks. The sync/atomic
package offers a variety of atomic operations for integers and pointers, allowing you to build lock-free data structures and improve the performance of your concurrent programs. By following best practices and understanding the limitations of atomic operations, you can write robust and efficient Go code.