SoFunction
Updated on 2025-03-03

The use of atomic operation of Go language

Overview

In daily development, concurrent scenarios are inevitable. The method of handling synchronization in Go language is usually to use locks, but if it is a single integer operation, using locks may cause greater performance overhead at this time, and the code also loses its aesthetics and elegance.

At this time, we can use the atomic operation that comes with Go. In the sync/atomic standard library of Go, atomic operation is a more basic technology than other synchronization technologies. Moreover, atomic operation is lock-free and is usually directly implemented through CPU instructions. If you look at the source code of other synchronization technologies, you can see that many technologies rely on atomic operations.

Synchronization issues

Before formally introducing atomic operations, first look at a piece of code. 100,000 coroutines are created in the code to accumulate a common variable x. There are 3 versions of the code in total. The first version is the normal version, the second version is the locked version, and the third version is the atomic operation version.

var (
	x    int64
	lock 
	wg   
)

// Normal versionfunc add() {
	x++
	()
}

// Mutex versionfunc mutexAdd() {
	()
	x++
	()
	()
}

// atomic versionfunc atomicAdd() {
	atomic.AddInt64(&x, 1)
	()
}

func main() {
	start := ()
	for i := 0; i < 100000; i++ {
		(1)
		//go add() // Normal version of add function, non-concurrency safe		//go mutexAdd() // Locked version add function, concurrency is safe, but locking performance is expensive		go atomicAdd() // Atomic operation version add function, concurrency security, performance is better than locked version	}
	()
	end := ()
	("Calculation result:", x)
	("Time consumed:", (start))
}

Run three versions in turn and the result is as follows:

# Normal version
Calculation result: 96725
Time consumed: 26.4237ms

# Locked version
Calculation result: 100000
Time consumed: 31.2588ms

# Atomic operation version
Calculation result: 100000
Time consumed: 27.3615ms

From the above results, we can see that the direct settlement result of the ordinary version is wrong. This will lead to calculation errors because the ordinary version is not concurrently safe. Both the locked version and the atomic operation version are calculated correctly, but the atomic operation version takes less time than the locked version (if the number is larger, the time may be more, you can try it yourself).

atomic

All atomic operations are under the atomic package. For int32, int64, uint32, uint64, uintptr and Pointer types, they have their corresponding atomic operations.

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *, new ) (old )

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *, old, new ) (swapped bool)

func AddInt32(addr *int32, delta int32) (new int32)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *) (val )

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *, val )

The above are all the methods in the atomic package, which are mainly divided into 5 types. The following is an explanation based on different types.

Load and Store

The Load and Store methods are mainly used to set and read numbers in a concurrent environment. Store means setting a value for the variable, and Load means reading the value of the variable.

var value int64

func main() {
	atomic.StoreInt64(&value, 1)
	val := atomic.LoadInt64(&value)
	("value: ", val)
}

Add

The Add method is simpler, which means adding a value to a variable. Using the Add method is concurrency-safe, and there will be no calculation errors in the ordinary version of the add function in the example above. If the variable is a signed integer type, you need to subtract the variable. You only need to pass the negative number into the second parameter when calling the Add method.

Swap and CompareAndSwap

Swap means exchange. Use the Swap method to modify the value of a variable, and at the same time, it will return the old value of the variable.

CompareAndSwap means comparison and exchange, and its function is similar to Swap. It is also a modification of the value of the variable. However, when calling CompareAndSwap, you need to pass in the new value that needs to be set and the expected old value. If the value of the current variable is the same as the expected old value, the variable will be modified and the new value will be returned, and whether the modification is successful will be returned.

var value int64 = 1

func main() {
	old := atomic.SwapInt64(&value, 2)
	("Old value: %d, value: %d\n", old, value)
	swapped := atomic.CompareAndSwapInt64(&value, 1, 3)
	("Modification result: %t, value: %d\n", swapped, value)
	swapped = atomic.CompareAndSwapInt64(&value, 2, 3)
	("Modification result: %t, value: %d\n", swapped, value)
}

In the above example, set the value to 1, first use the Swap method to change the Value to 2, and return the value before modification. Use CompareAndSwap again to change it to 3, but because the expected value 1 and the actual value of value 2 are not equal, the modification fails. If you call again the expected value is 2 and the actual value of value is 2, the modification is successful. The operation results are as follows:

Old value: 1, value: 2
Modification result: false, value: 2
Modification result: true, value: 3

New version structure type

In version 1.19, Go language has added structure types such as Int32 and Int64 to atomic. It is easier to use structure types to perform atomic operations. You don’t need to think about the same as before. Various types of methods are called from atomic every time to implement atomic operations. Instead, you only need to use the structure method to perform atomic operations directly.

var value atomic.Int64

func main() {
	(1)
	("value: ", ())
	n := (1)
	("value: ", n)
	old := (3)
	("Old value: %d, value: %d\n", old, ())
	swapped := (3, 4)
	("Modification result: %t, value: %d\n", swapped, ())
}

The latest writing structure type used in the example above is the following:

value:  1
value:  2               
Old value: 2, value: 3
Modification result: true, value: 4

This is the end of this article about the use of Go atomic operation atomic. For more related content on Go atomic operation, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!