Achieving High-Level Atomic Operations in Go
Alexey Shevelyov
Posted on October 12, 2023
Achieving High-Level Atomic Operations in Go
Go offers first-class support for concurrency, but mastering the art of concurrent programming involves intricate challenges. One of the most critical tasks is managing shared state across multiple Goroutines, and that's where atomic operations come into play. The Go standard library provides a range of low-level atomic operations that can be leveraged to build thread-safe applications. However, these operations often require a deep understanding of memory models and can be cumbersome to implement. In this article, we explore how to perform high-level atomic operations using the go.uber.org/atomic
package, making it easier to write safe and maintainable Go code.
The Scenario
Let's consider a real-world example involving a shared bank balance, a common scenario in financial software. Multiple threads or Goroutines may need to read or update this balance concurrently. While Go's standard library provides atomic operations to manage such shared state, it often lacks higher-level abstractions, which can make the code harder to read and maintain. That's where the go.uber.org/atomic
package comes in handy.
package main
import (
"fmt"
"go.uber.org/atomic"
)
var balance atomic.Int64
func updateBalance(minAmount int64, change int64) bool {
for {
// Load current balance atomically
currentBalance := balance.Load()
// Check if balance is greater than minAmount
if currentBalance < minAmount {
return false
}
// Calculate new balance
newBalance := currentBalance + change
// Try to update balance atomically
if balance.CompareAndSwap(currentBalance, newBalance) {
return true
}
}
}
func main() {
balance.Store(100)
success := updateBalance(50, -20)
fmt.Println("Operation Successful:", success)
fmt.Println("Updated Balance:", balance.Load())
}
How It Works
Atomic Loading and Storing
The balance.Load()
and balance.Store(100)
methods are examples of atomic operations that safely load and store the value of the balance
variable. These operations ensure that the value of balance
can be read or written safely, without being interrupted or accessed simultaneously by another Goroutine, thanks to the atomic methods provided by the go.uber.org/atomic
package.
Compare and Swap: The Cornerstone of Atomicity
The CompareAndSwap
method is the workhorse of our atomic operations. This method takes two arguments: the expected current value and the new value to set. It atomically checks if the current value matches the expected value and, if so, sets it to the new value. This operation is atomic, meaning it's done in a single, uninterruptible step, ensuring that no other Goroutine can change the balance
in the middle of the update.
This replaces the CAS
method we used earlier. While CAS
offered similar functionality, CompareAndSwap
is more idiomatic and aligns better with Go's naming conventions, making the code easier to understand.
Why go.uber.org/atomic
?
You might wonder why not stick with Go's built-in atomic package? The go.uber.org/atomic
package offers a more ergonomic API and additional type-safety, making it easier to write correct concurrent code. It provides a cleaner, more intuitive interface for dealing with atomic operations, as you can see from our example.
Managing shared state in a concurrent application is a complex but crucial aspect of Go programming. While Go's standard library offers low-level atomic operations, the go.uber.org/atomic
package provides a higher level of abstraction, making it easier to write, read, and maintain concurrent code. By understanding and leveraging these atomic operations, you can write more robust and efficient Go applications.
Posted on October 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.