头图

sync/atomic definition

Official document address: https://pkg.go.dev/sync/atomic@go1.18.1

The sync/atomic package in the Go language standard library provides low-level atomic memory primitives for implementing synchronization algorithms. The essence is to encapsulate the atomic operation instructions provided by the underlying CPU into Go function.

Using the atomic operations provided by sync/atomic can ensure that only one goroutine operates on the variable at any time, avoiding concurrency conflicts.

Use sync/atomic need special care, Go official recommends to use sync/atomic only in some low-level application scenarios, other scenarios suggest using channel or sync lock in the package.

Share memory by communicating; don't communicate by sharing memory.

sync/atomic provides 5 types of atomic operations and 1 Value type.

5 types of atomic operations

  • swap operation: SwapXXX
  • compare-and-swap operation: CompareAndSwapXXX
  • add operation: AddXXX
  • load operation: LoadXXX
  • store operation: StoreXXX

These types of atomic operations only support a few basic data types.

add操作的Addxxx函数只支持int32 , int64 , uint32 , uint64 , uintptr These 5 basic data types.

其它类型的操作函数只支持int32 , int64 , uint32 , uint64 , uintptr , unsafe.Pointer These 6 basic data types.

Value type

Since the above five types of atomic operations only support a few basic data types, in order to expand the scope of atomic operations, the Go team introduced a new type in the sync/atomic package of version 1.4 Value . Value type can be used to read (Load) and modify (Store) values of any type .

Go 1.4版本的Value类型Load Store 2个方法,Go 1.17版本Value类型新增了CompareAndSwap and Swap the 2 new methods.

sync/atomic practice

swap operation

swap操作支持int32 , int64 , uint32 , uint64 , uintptr , unsafe.Pointer These 6 basic data types correspond to 6 swap operation functions.

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

The function implemented by the swap operation is to replace the value in the memory pointed to by the addr pointer with the new value new , and then return the old value old , which is the following pseudo code Atomic implementation:

 old = *addr
*addr = new
return old

Let's take SwapInt32 for example:

 // swap.go
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var newValue int32 = 200
    var dst int32 = 100
    // 把dst的值替换为newValue
    old := atomic.SwapInt32(&dst, newValue)
    // 打印结果
    fmt.Println("old value: ", old, " new value:", dst)
}

The execution result of the above program is as follows:

 old value:  100  new value: 200

compare-and-swap operation

compare-and-swap(CAS)操作支持int32 , int64 , uint32 , uint64 , uintptr , unsafe.Pointer These 6 basic data types correspond to 6 compare-and-swap operation functions.

 func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (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)

The function implemented by the compare-and-swap operation is to first compare addr whether the value in the memory pointed to by the pointer is the old value old equal.

  • If they are equal, replace the value in the memory pointed to by the addr pointer with the new value new , and return true , indicating that the operation is successful.
  • If they are not equal, directly return false , indicating that the operation failed.

The compare-and-swap operation is an atomic implementation of the following pseudocode:

 if *addr == old {
    *addr = new
    return true
}
return false

Let's take CompareAndSwapInt32 as an example:

 // compare-and-swap.go
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var dst int32 = 100
    oldValue := atomic.LoadInt32(&dst)
    var newValue int32 = 200
    // 先比较dst的值和oldValue的值,如果相等,就把dst的值替换为newValue
    swapped := atomic.CompareAndSwapInt32(&dst, oldValue, newValue)
    // 打印结果
    fmt.Printf("old value: %d, swapped value: %d, swapped success: %v\n", oldValue, dst, swapped)
}

The execution result of the above program is as follows:

 old value: 100, swapped value: 200, swapped success: true

add operation

add操作支持int32 , int64 , uint32 , uint64 , uintptr 5种基本数据类型,对应There are 5 add operation functions.

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

The function implemented by the add operation is to add the value in the memory pointed to by the addr pointer and delta , and then return the new value, which is the atomic implementation of the following pseudocode:

 *addr += delta
return *addr

Let's take AddInt32 for example:

 // add.go
package main

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

var wg sync.WaitGroup

// 多个goroutine并发读写sum,有并发冲突,最终计算得到的sum值是不准确的
func test1() {
    var sum int32 = 0
    N := 100
    wg.Add(N)
    for i := 0; i < N; i++ {
        go func(i int32) {
            sum += i
            wg.Done()
        }(int32(i))
    }
    wg.Wait()
    fmt.Println("func test1, sum=", sum)
}

// 使用原子操作计算sum,没有并发冲突,最终计算得到sum的值是准确的
func test2() {
    var sum int32 = 0
    N := 100
    wg.Add(N)
    for i := 0; i < N; i++ {
        go func(i int32) {
            atomic.AddInt32(&sum, i)
            wg.Done()
        }(int32(i))
    }
    wg.Wait()
    fmt.Println("func test2, sum=", sum)
}

func main() {
    test1()
    test2()
}

The execution result of the above program is as follows:

 func test1, sum= 4857
func test2, sum= 4950

Note : For the test1 function, the result you get when you run it locally may be different from mine, and this value is not a fixed value.

load operation

load操作支持int32 , int64 , uint32 , uint64 , uintptr , unsafe.Pointer These 6 basic data types correspond to 6 load operation functions.

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

The function implemented by the load operation is to return the value in the memory pointed to by the addr pointer, which is an atomic implementation of the following pseudocode:

 return *addr

Let's take LoadInt32 as an example:

 // load.go
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var sum int32 = 100
    result := atomic.LoadInt32(&sum)
    fmt.Println("result=", result)
}

The execution result of the above program is as follows:

 result= 100

store operation

store操作支持int32 , int64 , uint32 , uint64 , uintptr , unsafe.Pointer These 6 basic data types correspond to 6 store operation functions.

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

The function implemented by the store operation is to modify the value in the memory pointed to by the addr pointer to val , which is an atomic implementation of the following pseudocode:

 *addr = val

Let's take StoreInt32 as an example:

 // store.go
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var sum int32 = 100
    var newValue int32 = 200
    // 将sum的值修改为newValue
    atomic.StoreInt32(&sum, newValue)
    // 读取修改后的sum值
    result := atomic.LoadInt32(&sum)
    // 打印结果
    fmt.Println("result=", result)
}

The execution result of the above program is as follows:

 result= 200

Value type

The sync/atomic package in the Go standard library provides the Value type, which can be used to concurrently read and modify any type of value.

Value types are defined as follows:

 // A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
    v any
}

Value类型有4个方法: CompareAndSwap , Load , Store , Swap ,定义如下:

 func (v *Value) CompareAndSwap(old, new any) (swapped bool)
func (v *Value) Load() (val any)
func (v *Value) Store(val any)
func (v *Value) Swap(new any) (old any)

Source code implementation: https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/sync/atomic/value.go

The following is a specific example: To do concurrent read and write to the map[string][string] type, in order to avoid locking, use the value type to read and modify map[string][string] .

 package main

import (
    "sync/atomic"
    "time"
)

func loadConfig() map[string]string {
    // 从数据库或者文件系统中读取配置信息,然后以map的形式存放在内存里
    return make(map[string]string)
}

func requests() chan int {
    // 将从外界中接收到的请求放入到channel里
    return make(chan int)
}

func main() {
    // config变量用来存放该服务的配置信息
    var config atomic.Value
    // 初始化时从别的地方加载配置文件,并存到config变量里
    config.Store(loadConfig())
    go func() {
        // 每10秒钟定时拉取最新的配置信息,并且更新到config变量里
        for {
            time.Sleep(10 * time.Second)
            // 对应于赋值操作 config = loadConfig()
            config.Store(loadConfig())
        }
    }()
    // 创建协程,每个工作协程都会根据它所读取到的最新的配置信息来处理请求
    for i := 0; i < 10; i++ {
        go func() {
            for r := range requests() {
                // 对应于取值操作 c := config
                // 由于Load()返回的是一个interface{}类型,所以我们要先强制转换一下
                c := config.Load().(map[string]string)
                // 这里是根据配置信息处理请求的逻辑...
                _, _ = r, c
            }
        }()
    }
}

Summary and Notes

  • Atomic operations are supported by the underlying CPU's atomic operation instructions.
  • 5 kinds of atomic operations and Value official document address: https://pkg.go.dev/sync/atomic@go1.18.1
  • CAS operation will have ABA problems
  • For the 386 processor architecture, the 64-bit atomic operation functions use CPU instructions that are only supported by the Pentium MMX or later processor model. For non-Linux ARM processor architectures, 64-bit atomic operation functions use CPU instructions supported by ARMv6k core or newer processor models. For ARM, 386, and 32-bit MIPS processor architectures, the caller of an atomic operation must align the 64-bit memory of the 64-bit word that is being accessed atomically. The first word of variables or allocated structures, arrays and slices can be considered 64-bit aligned. (This piece involves memory alignment, and a topic will be explained in detail later)

open source address

Articles and sample code are open sourced on GitHub: Beginner, Intermediate, and Advanced Tutorials in Go .

Official account: coding advanced. Follow the official account to get the latest Go interview questions and technology stacks.

Personal website: Jincheng's Blog .

Zhihu: Wuji .

References


coding进阶
116 声望18 粉丝