SoFunction
Updated on 2025-03-04

The difference between Go language atomic operation and mutex locks

Atomic operations are uninterruptible operations. The outside world cannot see the intermediate state of atomic operations. Either see that the atomic operations have been completed or that the atomic operations have been completed. During the execution of an atomic operation of a certain value, the CPU will never perform other operations on the value, so other operations are also atomic operations.

The atomic operations provided in the Go language are non-invasive, and related atomic functions are provided in the standard library code package sync/atomic.

Increase or decrease

The functions used for atomic operations for increment or decrease are all started with "Add", followed by the specific type name. For example, the following example is an atomic subtraction operation of type int64.

func main() {
   var  counter int64 =  23
   atomic.AddInt64(&counter,-3)
   (counter)
}
---output---
20

The first parameter of an atomic function is a pointer to a variable type because atomic operations need to know the location of the variable in memory, and then apply special CPU instructions, that is, atomic operations cannot be performed for variables that cannot obtain the memory storage address. The type of the second parameter is automatically converted to the same type as the first parameter. In addition, atomic operations will automatically assign the operation value to the variable without manually assigning it to the value ourselves.

For the second parameters of atomic.AddUint32() and atomic.AddUint64() are uint32 and uint64, so it is impossible to directly pass a negative value for subtraction. Go provides another way to implement it in a roundabout way: using the characteristics of two's complement

Note: Values ​​of types cannot be added or subtracted.

Compare And Swap

Abbreviated as CAS, several functions prefixed with "Compare And Swap" in the standard library code package sync/atomic are CAS operation functions, such as the following

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

The value of the first parameter is the pointer of the variable, the second parameter is the old value of the variable, and the third parameter refers to the new value of the variable.

Running process: After calling CompareAndSwapInt32, you will first determine whether the value on this pointer is equal to the old value. If it is equal, the value will be overwritten with the new value. If it is equal, the subsequent operations will be ignored. Returns a swapped boolean value indicating whether the value replacement operation has been performed.

Different from locks: Locks always assume that there will be concurrent operations to modify the value being operated, while CAS always assumes that the value has not been modified. Therefore, CAS has lower performance losses than locks. Locks are called pessimistic locks, while CAS is called optimistic locks.

Example of CAS usage

var value int32
func AddValue(delta int32)  {
   for {
      v:= value
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

As can be seen from the example, we need to use the for loop multiple times to determine whether the value has been changed. In order to ensure the success of the CAS operation, the loop is only exited when CompareAndSwapInt32 returns to true, which is similar to the spin behavior of a spin lock.

Loading and storage

When reading or writing a value does not mean that the value is the latest value. It may also be that concurrent write operations during the read or write process caused the original value to change. To solve this problem, the Go standard library code package sync/atomic provides a value of atomic reading (a function prefixed by Load) or writing (a function prefixed by Store)

Change the above example to atomic read

var value int32
func AddValue(delta int32)  {
   for {
      v:= atomic.LoadInt32(&value)
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

Atomic writing will always succeed because it does not need to care about what the original value is, and the old value must be paid attention to in CAS. Atomic writing contains two parameters. The following StroeInt32 is an example:

//The first parameter is the pointer of the value being operated, and the second is the new value of the value being operatedfunc StoreInt32(addr *int32, val int32) 

exchange

These operations are all functions that start with "Swap" and are called "atomic exchange operations". The functions are similar to the CAS operations and atomic write operations mentioned earlier.

func SwapInt32(addr *int32, new int32) (old int32)

Taking SwapInt32 as an example, the first parameter is a pointer of type int32, and the second is a new value. Atomic exchange operation does not need to care about the original value, but directly sets the new value, but returns the old value of the operated value.

Atomic value

There is an atomic value called Value in the standard library code package of Go language sync/atomic. It is a structure type used to store values ​​that need to be read and written by atoms. The structure is as follows

// Value provides atomically loading and storing values ​​of consistent types.// The value of zero returns nil from Load.//The value must not be copied after calling the Store.//The value must not be copied after the first use.type Value struct {
   v interface{}
}

It can be seen that there is a v interface{} in the structure, that is, the Value atomic value can hold any type of value that needs to be read and written by atoms.

How to use it is as follows:

var Atomicvalue  

This type has two public pointer methods

//The atom reads the value stored in the atomic value instance, returns a value of type interface{}, and does not accept any parameters.// If the value has not been stored through the store method, nil will be returnedfunc (v *Value) Load() (x interface{})

//The atomic store a value in the atomic instance, receives a parameter of type interface{} (cannot be nil), and will not return any valuefunc (v *Value) Store(x interface{})

Once the atomic value instance stores a value of a certain type, the value stored in the Store must then be consistent with the type, otherwise panic will be triggered.

Strictly speaking, once a type of variable is declared, it should not be copied elsewhere. For example: assigning values ​​to other variables as source values, passing them as parameters to functions, returning from functions as result values, and passing them as element values ​​through channels, all of which will cause the copy of values.

However, this problem will not exist in the type pointer type variable. The reason is that copying the structure will not only generate a copy of the value, but also generate a copy of the fields in it. In this way, the value changes caused by concurrency have nothing to do with the original value.

See the following small example

func main() {
   var Atomicvalue  
   ([]int{1,2,3,4,5})
   anotherStore(Atomicvalue)
   ("main: ",Atomicvalue)
}

func anotherStore(Atomicvalue )  {
   ([]int{6,7,8,9,10})
   ("anotherStore: ",Atomicvalue)
}
---output---
anotherStore:  {[6 7 8 9 10]}
main:  {[1 2 3 4 5]}

The difference between atomic operations and mutex locks

A mutex is a data structure that allows you to perform a series of mutex operations. Atomic operations are mutually exclusive single operations, which means no other thread can interrupt it. So what is the difference between the atomic operation in the Go language and the synchronization lock provided by the sync package?

First of all, the advantage of atomic operations is that they are lighter, such as CAS can complete concurrent and safe value replacement operations without forming critical areas and creating mutexes. This can greatly reduce the loss of synchronization to program performance.

Atomic operation also has disadvantages. Taking CAS operation as an example, the practice of using CAS operation tends to be optimistic, always assuming that the value being operated has not been changed (that is, it is equal to the old value), and once the authenticity of this assumption is confirmed, the value is replaced immediately. Then, when the value being operated is frequently changed, the CAS operation is not so easy to succeed. The practice of using mutex locks tends to be pessimistic. We always assume that there will be concurrent operations to modify the value of the operation and use locks to place the related operations into the critical area for protection.

So to summarize the differences between atomic operations and mutex locks:

A mutex is a data structure that allows a thread to execute key parts of a program and complete multiple mutually exclusive operations.
An atomic operation is a single mutex operation for a certain value.
Mutex locks can be understood as pessimistic locks. Shared resources are only used by one thread at a time. Other threads are blocked. After use, the resources are transferred to other threads.
The atomic package provides the underlying atomic memory primitive, which is useful for the implementation of synchronization algorithms. These functions must be used very carefully. Improper use will increase the overhead of system resources. For the application layer, it is best to use the functions provided in the channel or sync package to complete synchronization operations.

The views on atomic packages are also discussed in Google's email group, one of which is explained as follows:

This packaging should be avoided. Or, read the "Atomic Operations" chapter of the C++11 standard; if you understand how to use these operations safely in C++, you have the ability to use Go's sync/atomic package safely.

This is the end of this article about the difference between Go atomic operation and mutex locks. 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!