Atomic Operations Provided In The sync/atomic Standard Package

Atomic operations are more primitive than other synchronization techniques. Atomic operations are lockless and generally implemented at hardware level. In fact, atomic operations are often used in implementing other synchronization techniques.

One design goal of Go language is to easy using. So unlike C++ and some other languages, Go doesn't provide low level ways to control memory ordering in using atomic operations. The sync/atomic standard package provides some functions and the atomic.Value type to do atomic operations.

Please note, many examples below are not concurrent programs, they are just for atomic function demonstration purpose.

Overview Of Atomic Operations Provided In Go

The sync/atomic standard package provides the following five atomic functions for an integer type T, where T must be any of int32, int64, uint32, uint64 and uintptr.
func AddT(addr *T, delta T)(new T)
func LoadT(addr *T) (val T)
func StoreT(addr *T, val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)
For example, the following five functions are provided for type int32.
func AddInt32(addr *int32, delta int32)(new int32)
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
func SwapInt32(addr *int32, new int32) (old int32)
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
Except for AddT, the other four functions mentioned above also provided for type unsafe.Pointer.
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func SwapPointer(addr *unsafe.Pointer, new T) (old unsafe.Pointer)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

Values of any pointer type can be converted to unsafe.Pointer, and vice versa. So we can view unsafe.Pointer as any pointer type, just like the void* in C. The above foure functions can be used to atomic pointer operations. The reason of why there is not a AddPointer function is pointer types don't support arithmetic operations in Go.

The sync/atomic standard package also provides a type Value. Type *Value has two methods, Load and Store. A Value value can be used to atomically load and store values of any type.
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})

Followings are some examples to show how to use the atomic operations provided in Go.

Atomic Operations On Integer Values

The following example shows how to do the add atomic operation on an int32 value by using the AddInt32 function. In this example, 1000 new goroutines are created by the main goroutine. Atomic operations guarantee that there are no data races among these goroutines. In the end, 1000 is guaranteed to be printed.
package main

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

func main() {
	var n int32
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			atomic.AddInt32(&n, 1)
			wg.Done()
		}()
	}
	wg.Wait()
	
	fmt.Println(atomic.LoadInt32(&n)) // 1000
}
The StoreT and LoadT atomic functions are often used to implement the setter and getter methods of a type if the values of the type are accessed concurrently. For example,
type Page struct {
	views uint32
}

func (page *Page) SetViews(n uint32) {
	atomic.StoreUint32(&page.views, n)
}

func (page *Page) Views() uint32 {
	return atomic.LoadUint32(&page.views)
}
For signed types int32 and int64, the second arguments for an AddT function calls can be a negative value, to do atomic decrease operations. But how to do decrease atomic operations for values of an unsigned type T, such as uint32, uint64 and uintptr? There are two circumstances for the second unsigned arguments:
  1. for an unsigned variable v of type T, -v is legal in Go. So we can just pass -v as the second arguments of AddT calls.
  2. for a positive constant integer c, -v is illegal to be used as the second arguments of AddT calls. We can used ^T(c-1) as the second arguments instead.

We can also assign a constant integer to an unsigned variable v, then pass -v as the second argument, but this workaround is less efficient than the ^T(c-1) trick.

This ^T(c-1) trick also works for unsigned variables, but ^T(v-1) is less efficient than -v.

In the form ^T(c-1), if c is a typed value and its type matches the second perameter type of function AddT, then the form can shortened as ^(c-1).

Example:
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var (
		n uint64 = 97
		m uint64 = 1
		k int    = 2
	)
	const (
		a        = 3
		b uint64 = 4
		c uint32 = 5
		d int    = 6
	)
	
	atomic.AddUint64(&n, -m);             fmt.Println(n) // 96 (97 - 1)
	atomic.AddUint64(&n, -uint64(k));     fmt.Println(n) // 94 (95 - 2)
	atomic.AddUint64(&n, ^uint64(a - 1)); fmt.Println(n) // 91 (94 - 3)
	atomic.AddUint64(&n, ^(b - 1));       fmt.Println(n) // 87 (91 - 4)
	atomic.AddUint64(&n, ^uint64(c - 1)); fmt.Println(n) // 82 (87 - 5)
	atomic.AddUint64(&n, ^uint64(d - 1)); fmt.Println(n) // 76 (82 - 6)
	x := b; atomic.AddUint64(&n, -x);     fmt.Println(n) // 72 (76 - 4)
	atomic.AddUint64(&n, ^(m - 1));       fmt.Println(n) // 71 (72 - 1)
	atomic.AddUint64(&n, ^uint64(k - 1)); fmt.Println(n) // 69 (71 - 2)
}

The SwapT functions are like the StoreT functions, but return the old values.

The CompareAndSwapT functions only apply store operations when the current values match the passed old values. The bool return results of the CompareAndSwapT functions indicate whether or not the store operations are applied.

Example:
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var n int64 = 123
	var old = atomic.SwapInt64(&n, 789)
	fmt.Println(n, old) // 789 123
	fmt.Println(atomic.CompareAndSwapInt64(&n, 123, 456)) // false
	fmt.Println(n) // 789
	fmt.Println(atomic.CompareAndSwapInt64(&n, 789, 456)) // true
	fmt.Println(n) // 456
}

Please note, up to now (Go 1.10), atomic operations for 64-bit words, a.k.a., int64 and uint64 values, require the 64-bit words are 8-byte aligned in memory. Please read memory layout for details.

Atomic Operations On Pointer Values

Above has mentioned that there are four functions provided in the sync/atomic standard package to do atomic pointer operations, with the help of the unsafe.Pointer type.

In Go, values of any pointer type can be explicitly converted to unsafe.Pointer, and vice versa. So values of *unsafe.Pointer type can also be explicitly converted to unsafe.Pointer, and vice versa.

The following example is not a concurrent program. It just shows how to do atomic pointer operations. In this example, T can be any type.
package main

import (
	"fmt"
	"sync/atomic"
	"unsafe"
)

type T struct {a, b, c int}
var pT *T

func main() {
	var unsafePPT = (*unsafe.Pointer)(unsafe.Pointer(&pT))
	var ta, tb T
	// store
	atomic.StorePointer(unsafePPT, unsafe.Pointer(&ta))
	// load
	pa1 := (*T)(atomic.LoadPointer(unsafePPT))
	fmt.Println(pa1 == &ta) // true
	// swap
	pa2 := atomic.SwapPointer(unsafePPT, unsafe.Pointer(&tb))
	fmt.Println((*T)(pa2) == &ta) // true
	// compare and swap
	b := atomic.CompareAndSwapPointer(unsafePPT, pa2, unsafe.Pointer(&tb))
	fmt.Println(b) // false
	b = atomic.CompareAndSwapPointer(unsafePPT, unsafe.Pointer(&tb), pa2)
	fmt.Println(b) // true
}

Yes, it is quite verbose to use the pointer atomic functions. In fact, not only are the uses verbose, but they are not protected by the Go 1 compatibility guidelines, for these uses require to import the unsafe standard package.

Personally, I think the possibility is small that the valid pointer value atomic operations used in the above example will become invalid later. Even if they becomes invalid later, the go fix command in the official Go SDK should fix them with a later alternative new valid way. But, this is just my opinion, which is not authoritative.

If you do warry about the future validity of the pointer atomic operations used in the above example, you can use the atomic operations introduced in the next section for pointers, though the to be introduced operations are a little less efficient than the ones introduced in the current section.

Atomic Operations On Values Of Arbitrary Types

The Value type can be used to atomically load and store values of any type. Type *Value has two methods, Load and Store.

Add and Swap methods are not available for type *Value.

The input parameter type and output result type of the Load and Store methods are both interface{}. So a call of the Store can take a value of any type. But for an addressable Value value v, once v.Store, a shorthand of (&v).Store, has ever been called, then the subsequent v.Store calls must also take argument values of the same type as the first v.Store call, otherwise, panic will hanppen. A nil interface{} argument will also make a v.Store call panic.

Example (not a concurrent one, just for demo purpose):
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	type T struct {a, b, c int}
	var ta = T{1, 2, 3}
	var v atomic.Value
	v.Store(ta)
	var tb = v.Load().(T)
	fmt.Println(tb)       // {1 2 3}
	fmt.Println(ta == tb) // true
	
	v.Store("hello") // will panic
}

We can also use the atomic pointer functions explained in the last section to do atomic operations for values of any type, with one more level indirection. Both ways have their repective advantages and disadvantages. Which way should be used depends on the requirements in practice.

Memory Order Guarantee Made By Atomic Operations In Go

The offical documentations don't specify any memory order guarantees made by the sync/atomic standard package. Please read Go memory model for details.

Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies description flaws and code bugs.