golang

Golang Atomic Examples

In Go, the sync/atomic package provides a set of functions to perform the atomic operations to ensure the safe and consistent behavior in concurrent scenarios. The sync/atomic package in Go provides a range of functions to perform the atomic operations on specific data types including integers and pointers. Atomic operations ensure that a particular operation is executed as a single, indivisible unit without any interference from the other concurrent operations. In this post, we’ll look at the atomic package operations using the real-world scenarios.

Example 1: Golang Atomic LoadInt32() Function

Begin with the atomic.LoadInt32() function example here. The atomic.LoadInt32() function is part of the sync/atomic package and is used to atomically load (read) the value of a 32-bit integer variable. Here, we can get more about the function via the following example:

package main
import "fmt"
import "sync/atomic"
func main() {
    var n1 int32 = 123
    var n2 int32 = 321
    loadNum1 := atomic.LoadInt32(&n1)
    loadNum2 := atomic.LoadInt32(&n2)
    fmt.Println(loadNum1)
    fmt.Println(loadNum2)
}

In the code, we define the sync/atomic package which provides functionality for atomic memory operations. Here, we import this package to gain an access to its functions. Moving on, in the main() function, we declare two int32 variables – “n1” and “n2” – with the values, respectively. These variables hold the integers that we intend to load atomically.

After that, “loadNum1” and “loadNum2” are declared to hold the loaded values of “n1” and “n2”, respectively. The results of the atomic loads are stored in these variables. Then, we employ the atomic.LoadInt32 function to atomically load the values of “n1” and “n2”. It takes the memory address of the variables as a parameter which is indicated by the & operator to directly access the underlying memory location.

The retrieved output indicates that the values of “n1” and “n2” are successfully loaded into “loadNum1” and “loadNum2”:

Example 2: Golang Atomic LoadInt64() Function

Subsequently, the atomic.LoadInt64() function is also a part of the sync/atomic package and is used to atomically load (read) the value of a 64-bit integer variable. The function can be clearer from the given code.

package main
import "fmt"
import "sync/atomic"
func main() {
    var int1 int64 = 93882473249
    var int2 int64 = 18470101555
    Val1 := atomic.LoadInt64(&int1)
    Val2 := atomic.LoadInt64(&int2)
    fmt.Println(Val1)
    fmt.Println(Val2)
}

In the code, we define two int64 variables – “int1” and “int2” – which are initialized with the long integer values, respectively. The atomic.LoadInt64() function is then used to atomically load the values of “int1” and “int2”. Similar to the previous example, it takes the memory address of the variable as its argument and returns the current value that is stored at that memory location. Here, it loads the values of “int1” and “int2” into the “Val1” and “Val2” variables, respectively. Subsequently, the fmt.Println() function is used to print the values of “Val1” and “Val2”.

The output shows the values of “Val1” and “Val2” that were loaded atomically from “int1” and “int2”, respectively:

Example 3: Golang Atomic StoreInt64() Function

In addition, to store an int64 or int32 atomically, we can use the atomic.StoreInt64() function or atomic.StoreInt32() function. The example that is given here uses the atomic.StoreInt64() function.

package main
import (
    "fmt"
    "sync/atomic"
)
func main() {
    var (
        a int64
        b int64
    )

    atomic.StoreInt64(&a, 4555454555)
    atomic.StoreInt64(&b, 13388)
    fmt.Println(atomic.LoadInt64(&a))
    fmt.Println(atomic.LoadInt64(&b))
}

In the code, we first create two variables – “a” and “b” – which are declared with the type “int64”. These variables store the 64-bit signed integer values. After that, we use the atomic.StoreInt64() function to atomically store a value into an “int64” variable. The variable address that has to be modified is provided as the first parameter by the “&” operator. A value must be stored, which is the second argument. After storing the variables in the atomic.StoreInt(), we load the values of variables “a” and “b” into the atomic.LoadInt64() function, respectively.

The displayed output is the loaded variable values using the atomic operation:

Example 4: Golang Atomic CompareAndSwapInt64() Function

Certainly, the atomic package in Go provides the CompareAndSwapInt64() function to perform an atomic compare-and-swap operation on an “int64” value. Consider the example of the atomic CompareAndSwapInt() function.

package main
import (
   "fmt"
   "sync/atomic"
)

func main() {
   var c int64 = 0
   atomic.AddInt64(&c, 1)
   awaited := int64(0)
   new := int64(1)
   atomic.CompareAndSwapInt64(&c, awaited, new)
   val := atomic.LoadInt64(&c)
   fmt.Println(val)
}

In the code, we begin with the declaration of the “c” variable of type “int64” and initialize it to “0”. We then use the atomic.AddInt64() function to atomically increment the value of “c” by “1”. This function ensures that the increment operation is executed atomically to prevent the concurrent access issues.

After that, we define the “awaited” and “new” variables, both of type “int64”. Here, the “awaited” variable is set to 0, and the “new” variable is set to 1. We then employ the atomic.CompareAndSwapInt64() function to compare the value of “c” with “awaited” (0) and swap it with “new” (1) if the values match. Here, the function returns a Boolean which indicates whether the swap is successful. In this case, if “c” is 0, it will be swapped with 1. Thereafter, with the use of the atomic.LoadInt64() function, we atomically load the value of “c” into the “val” variable

The output generats the value of “c” as 1 which indicates that the swap is successfully done:

Example 5: Golang Atomic AddUint32() Function

Furthermore, we have the AddUint32() function for atomic addition operations on “uint32” variables. This function allows us to add a value to a “uint32” variable atomically. Here’s the program where we use the AddUnit32() function to add a “unit32” variable value:

package main
import (
    "fmt"
    "sync"
    "sync/atomic"
)
func main() {
    var x uint32
    var wait sync.WaitGroup
    for i := 0; i < 20; i += 1 {
        wait.Add(1)
        go func() {
            atomic.AddUint32(&x, 1)
            wait.Done()
        }()
    }
    wait.Wait()
    fmt.Println("Atomic Variable Value:", x)
}

In the code, we have an “x” variable of type “uint32”. This variable is incremented atomically. Here, we use a “for” loop to spawn “20” goroutines that concurrently increment the value of “x”. Inside the goroutine, we use the atomic.AddUint32() function to atomically increment the value of “x” by 1. This function ensures that the increment operation is executed atomically to prevent the race conditions.

Next, we call the “wait.Add(1)” to add a counter to the WaitGroup which indicates that a goroutine is about to start. We use an anonymous function for the goroutine and call the wait.Done() when the goroutine completes the decrement of the WaitGroup counter. Lastly, we call the “wait.Wait()” function to wait until all the spawned goroutines have finished executing. This ensures that the program doesn't exit prematurely.

The following output represents the 20 goroutines that concurrently increment the “x” variable using the atomic operations:

Conclusion

The atomic operation is explored in this article with running examples. These examples include the atomic load and store operation, compare and swap operation, and add unit operation. Understanding the atomic operations and applying them appropriately can significantly improve the reliability and correctness of the concurrent Go applications.

About the author

Kalsoom Bibi

Hello, I am a freelance writer and usually write for Linux and other technology related content