I have used sync.atomic
for the first time in my work. It’s lighter than Mutex. If the program requires speed and it doesn’t require a function call, atomic is an alternative way of Mutex.
How to use atomic for int value
Let’s look at the example first. It’s easy. It contains sync.WaitGroup
in order to wait for all the goroutines.
import (
"sync"
"sync/atomic"
)
func WriteInGoroutine() int64 {
var ops atomic.Int64 // atomic here
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
ops.Add(1) // same as ops += 1
}
wg.Done()
}()
}
wg.Wait()
return ops.Load() // return 50000
}
If ops
is a normal int64 data type, the result won’t be 50000 because multiple goroutines try to update the value at the same time without interacting with other goroutines.
By the way, atomic struct has the following methods. The methods that are often used are Add
to update data, Load
to read it, and Store
to assign a new value or initialize.
func (x *Int64) Add(delta int64) (new int64)
func (x *Int64) CompareAndSwap(old, new int64) (swapped bool)
func (x *Int64) Load() int64
func (x *Int64) Store(val int64)
func (x *Int64) Swap(new int64) (old int64)
It’s easy enough.
Can a normal int value be updated in the atomic way?
There might be a case where we declare a variable and don’t want to change the data type so that the existing code can still be used without modification.
Use e.g. atomic.AddInt64
function in this case.
func WriteInGoroutine2() int64 {
var ops int64 // normal int64 value
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
atomic.AddInt64(&ops, 1) // static function
}
wg.Done()
}()
}
wg.Wait()
return ops // 50000
}
In this way, we can keep the existing code but update the value in an atomic way.
Handling struct with atomic
We can use atomic.Value
type if we want to use struct. Assign the value by using Store()
method. The data can be read by Load()
method.
func storeDifferentType() {
fmt.Println("--- storeDifferentType ---")
var value atomic.Value
fmt.Println(value.Load()) // nil
myData := myStruct{name: "Yuto", age: 36}
value.Store(myData)
loadedData := value.Load()
fmt.Println(loadedData) // { name: Yuto, age: 36 }
fmt.Println(loadedData.(myStruct)) // { name: Yuto, age: 36 }
}
atomic.Value
doesn’t have a data type. It looks like a different value can be assigned but it’s of course not possible.
func storeDifferentType() {
fmt.Println("--- storeDifferentType ---")
var value atomic.Value
fmt.Println(value.Load()) // nil
myData := myStruct{name: "Yuto", age: 36}
value.Store(myData)
loadedData := value.Load()
fmt.Println(loadedData) // { name: Yuto, age: 36 }
fmt.Println(loadedData.(myStruct)) // { name: Yuto, age: 36 }
person := person{
Name: "Yuto",
Age: 36,
Gender: "man",
}
// panic: sync/atomic: swap of inconsistently typed value into Value
value.Store(person)
fmt.Println(value.Load())
}
It panics if a different struct is assigned.
A new struct needs to be assigned by using CompareAndSwap()
or Swap()
method to update the value. A method in the struct should not be called to update the property because its method call is no longer an atomic process. Get the struct from atomic.Value is atomic but calling the method is not. It should be handled as an immutable variable.
Handling a pointer with atomic
Handling a pointer is almost the same as atomic.Value
. The only difference is to give the data type.
func atomicPointer() {
fmt.Println("--- atomicPointer ---")
var pointerValue atomic.Pointer[myStruct]
fmt.Println(pointerValue.Load() == nil) // true
old := myStruct{name: "name-1", age: 1}
pointerValue.Store(&old)
fmt.Println("whole struct: ", pointerValue.Load()) // whole struct: { name: name-1, age: 1 }
fmt.Println("name: ", pointerValue.Load().name) // name: name-1
fmt.Println("age: ", pointerValue.Load().age) // age: 1
fmt.Println("--- CompareAndSwap ---")
newData := myStruct{name: "name-1", age: 2}
pointerValue.CompareAndSwap(&old, &newData)
fmt.Println("whole struct: ", pointerValue.Load()) // whole struct: { name: name-1, age: 2 }
fmt.Println("name: ", pointerValue.Load().name) // name: name-1
fmt.Println("age: ", pointerValue.Load().age) // age: 2
}
Since Pointer has a data type, it’s more readable than using Value in my opinion.
Performance comparison between Mutex and atomic
To compare the performance, I wrote the following code. The methods used below are already shown above.
import (
"play-with-go-lang/utils"
"testing"
)
func BenchmarkWriteWithAtomic(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = utils.WriteInGoroutine()
}
}
func BenchmarkWriteWithAtomic2(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = utils.WriteInGoroutine2()
}
}
func BenchmarkWriteWithMutex(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = utils.WriteInGoroutineWithMutex()
}
}
The result is as follows.
$ go test ./benchmark -bench Write
goos: linux
goarch: amd64
pkg: play-with-go-lang/benchmark
cpu: Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz
BenchmarkWriteWithAtomic-12 1567 816342 ns/op
BenchmarkWriteWithAtomic2-12 1396 820650 ns/op
BenchmarkWriteWithMutex-12 306 3964910 ns/op
PASS
ok play-with-go-lang/benchmark 4.206s
Mutex is about 4 times slower. If the application requires performance and the logic is simple enough, atomic is a better choice.
Comments