likes
comments
collection
share

一文搞定 Go 原子操作计算机科学中的原子操作,包括其定义、特点、应用、与锁的区别,以及在 Go 语言中的实现原理和相关

作者站长头像
站长
· 阅读数 49

什么是原子操作

在计算机科学中,原子操作指的是一个或一系列不可被中断的操作。也就是说,这些操作在执行过程中不会被分割成更小的部分,也不会被其他操作干扰或中断。

原子操作的特点

原子操作具有以下几个重要特点:

  1. 完整性:原子操作要么完全执行,要么完全不执行,不会出现执行到一半的情况。

  2. 不可分割性:不能被其他操作打断,整个操作作为一个独立的、不可分割的单元执行。

  3. 原子性:保证操作的原子性是为了确保数据的一致性和正确性。例如,在对一个共享变量进行修改时,如果这个修改操作不是原子的,可能会导致数据的不一致性。

原子操作的应用

原子操作在多线程编程和并发环境中非常重要。在多个线程同时访问和修改共享资源时,使用原子操作可以避免出现竞争条件和数据不一致的问题。常见的原子操作包括原子的变量赋值、原子的计数器递增或递减等。

例如,在一个多线程的计数器程序中,如果没有使用原子操作来增加计数器的值,可能会出现多个线程同时修改计数器导致结果错误的情况。而使用原子操作可以确保计数器的值在任何时候都是正确的。

package main

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

func main() {
    var sum1 int32 = 0
    var sum2 int32 = 0
    var wg sync.WaitGroup
    wg.Add(1000)

    for i := 0; i < 1000; i++ {
        go func() {
            defer wg.Done()
            sum1++
            atomic.AddInt32(&sum2, 1)
        }()
    }

    wg.Wait()
    fmt.Println("非原子操作:", sum1) 
    fmt.Println("原子操作:", sum2) 
}

$ go run main.go
非原子操作:989 
原子操作:1000

原子操作与锁的区别

原子操作和互斥锁均可用于在并发环境中保护共享资源,不过它们在应用场景、实现机制、性能等方面存在一定的差异:

差异点原子操作互斥锁
应用场景适用于对单个变量或简单数据结构的操作,尤其是在高并发场景下需要频繁进行的简单操作。适用于对复杂数据结构或一段代码块的同步,当需要确保一组操作的原子性和一致性时,互斥锁更为合适。例如,对一个链表的插入和删除操作,或者对多个变量的同时修改。
实现机制通过底层硬件指令实现,不需要复杂的同步逻辑。通常基于信号量、原子操作、线程调度等一系列复杂操作实现的。
性能通常性能较高,因为它们直接在硬件层面实现,避免了上下文切换和线程阻塞的开销。相对原子操作性能较低。当多个线程竞争锁时,会导致线程阻塞和唤醒,这会带来一定的开销。

Go 原子操作

Go 原子操作是通过 sync/atomic 包实现的,主要包括了 Add、CompareAndSwap、Swap、Load、Store 等原子操作,go 1.23 还引入 And、Or 操作。

AddT操作

AddT是一系列函数的集合,可以操作int32、int64、uint32、uint64、uintptr类型的变量,其中 int32 和 int64 可以是负数,如果是负数就能实现相减的效果。

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)

AddT 操作与下面的伪代码是等效,但是需要把下面代码当成一个原子的来看:

func AddT(addr *T, delta T) (new T) {
    *addr += delta
    return *addr
}

CompareAndSwapT 操作

CompareAndSwapT 字面含义是比较并交换,在Go的 sync 包中有广泛的应用,常用来做有条件的更新操作,有点类似数据库操作中的乐观锁。

CompareAndSwapT 同样也是 CompareAndSwap 的函数集:

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)

CompareAndSwapT 的伪代码如下:

  1. 如果 *addr 与 old 相等,则把 new 赋值给 *addr, 并返回 true,代表交换成功;

  2. 如果不相等则直接返回 false,代表没有进行交换。

func CompareAndSwapT(addr *T, old, new T) (swapped bool)
    if *addr == old {
        *addr = new
        return true
    }
    
    return false
}

LoadT 操作

原子性的读取 addr 指向的数据,能保证读取 addr 指向的数据时,没有其他程序读取 addr 指向的数据。

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)

LoadT的等效伪代码如下:

func LoadT(addr *T) (val T) {
    return *addr
}

StoreT 操作

Store 可以将 val 值原子性的保存到 *addr 中。

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

StoreT 的伪代码:

func StoreT(addr *T, val T) {
    *addr = val
}

SwapT 操作

SwapT 以原子方式将新值存储到 addr 中并返回先前的 addr 值,同样 SwapT 也是一系列函数的合集:

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)

SwapT 的伪代码:

func SwapT(addr *T, new T) (old T)
    old = *addr
    *addr = new
    return old
}

AndT 操作

AndT 是 go1.23 新添加的特性,用来实现原子的按位与运算:

func AndInt32(addr *int32, mask int32) (old int32)
func AndInt64(addr *int64, mask int64) (old int64)
func AndUint32(addr *uint32, mask uint32) (old uint32)
func AndUint64(addr *uint64, mask uint64) (old uint64)
func AndUintptr(addr *uintptr, mask uintptr) (old uintptr)

AndT 的伪代码:

func AndT(addr *T, mask T) (old T) {
    old = *addr 
    *addr = *addr & mask
    return old
}

OrT 操作

OrT 同样是 go1.23 新添加的特性,用来实现原子的按位或运算:

func OrInt32(addr *int32, mask int32) (old int32)
func OrInt64(addr *int64, mask int64) (old int64)
func OrUint32(addr *uint32, mask uint32) (old uint32)
func OrUint64(addr *uint64, mask uint64) (old uint64)
func OrUintptr(addr *uintptr, mask uintptr) (old uintptr)

OrT 的伪代码:

func OrT(addr *T, mask T) (old T) {
    old = *addr 
    *addr = *addr | mask
    return old
}

Go 原子操作实现原理

下面的内容会涉及汇编的相关知识,不了解汇编的小伙伴可以阅读《Go 语言进阶秘籍——Plan9 汇编

上面提到的原子操作是基于硬件指令实现的,我们在 src/internal/runtime/atomic 目录中可以找到具体的汇编代码,Go 是跨平台的编程语言,不同的平台有不同的实现:

一文搞定 Go 原子操作计算机科学中的原子操作,包括其定义、特点、应用、与锁的区别,以及在 Go 语言中的实现原理和相关

一文搞定 Go 原子操作计算机科学中的原子操作,包括其定义、特点、应用、与锁的区别,以及在 Go 语言中的实现原理和相关

其中 atomic_386.go 、 atomic_386.s 是 386 平台原子操作函数的定义与实现,这些原子操作的核心是 Cas 、Xadd、Xchg、Store 、Load 、 And、 Or 等几个基本函数。

Cas 函数

Cas 函数是比较并交换的汇编实现,其中最核心的 LCOK 指令:

TEXT ·Cas(SB), NOSPLIT, $0-13
    MOVL   ptr+0(FP), BX
    MOVL   old+4(FP), AX
    MOVL   new+8(FP), CX
    LOCK
    CMPXCHGL   CX, 0(BX)
    SETEQ  ret+12(FP)
    RET
  1. MOVL ptr+0(FP), BX:将栈帧指针 FP 加上偏移量 0 处的值(ptr指向的值)移动到寄存器 BX 中。

  2. MOVL old+4(FP), AX:将 FP 加上偏移量 4 处的值(old的值)移动到寄存器 AX 中,可能是一个旧值。

  3. MOVL new+8(FP), CX:将 FP 加上偏移量 8 处的值(new的值)移动到寄存器 CX 中,可能是一个新值。

  4. LOCK:是一个指令前缀,其后必须跟一条“读-改-写”性质的指令,它们可以是ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG。该指令是一种锁定协议,用于封锁总线,禁止其他 CPU 对内存的操作来保证原子性。

  5. CMPXCHGL CX, 0(BX):CMPXCHGL,L代表4个字节。该指令会把AX(累加器寄存器)中的内容(old)和第二个操作数(0(BX))中的内容(ptr所指向的数据)比较。如果相等,则把第一个操作数(CX)中的内容(new)赋值给第二个操作数,同时将 ZF(零标志位)置为 1。

  6. SETEQ ret+12(FP):如果零标志位为 1(即比较并交换成功),将栈帧指针 FP 加上偏移量 12 处的值设置为 1(可能是一个返回值的存储位置),否则为 0。

  7. RET:从函数返回。

Xadd 函数

Xadd 函数实现了一个原子性的加法操作并返回结果。

TEXT ·Xadd(SB), NOSPLIT, $0-12
    MOVL   ptr+0(FP), BX
    MOVL   delta+4(FP), AX
    MOVL   AX, CX
    LOCK
    XADDL  AX, 0(BX)
    ADDL   CX, AX
    MOVL   AX, ret+8(FP)
    RET
  1. MOVL ptr+0(FP), BX:将栈帧指针 FP 加上偏移量 0 处的值(即参数 ptr)移动到寄存器 BX 中。这里假设 BX 是一个 32 位寄存器,用于存储指针。

  2. MOVL delta+4(FP), AX:将栈帧指针 FP 加上偏移量 4 处的值(即参数 delta)移动到寄存器 AX 中。这里假设 AX 是一个 32 位寄存器,用于存储增量值。

  3. MOVL AX, CX:将寄存器 AX 的值复制到寄存器 CX 中。

  4. LOCK:指令前缀,用于确保后续的指令执行是原子性的。

  5. XADDL AX, 0(BX):对寄存器 AX 和内存地址为 BX 指向的地址处的值进行原子性的加法操作,并将结果存储回内存地址中。这里 BX 是指针,指向要进行加法操作的内存位置。

  6. ADDL CX, AX:将寄存器 CX(保存着原始的增量值)和寄存器 AX(保存着加法操作后的结果)的值相加。这一步可能是为了计算最终的返回值,但具体目的可能取决于上下文。

  7. MOVL AX, ret+8(FP):将寄存器 AX 的值移动到栈帧指针 FP 加上偏移量 8 处,可能是作为返回值的存储位置。

  8. RET:从函数返回。

Xchg 函数

Xchg 函数实现了一个原子性的交换操作。它接受一个指向特定数据类型的指针 ptr 和一个新值 new,将 ptr 所指向的内存位置的值与 new 进行原子性交换,并返回原始内存位置的值。

TEXT ·Xchg(SB), NOSPLIT, $0-12
    MOVL   ptr+0(FP), BX
    MOVL   new+4(FP), AX
    XCHGL  AX, 0(BX)
    MOVL   AX, ret+8(FP)
    RET
  1. MOVL ptr+0(FP), BX:将栈帧指针 FP 加上偏移量 0 处的值(即参数 ptr)移动到寄存器 BX 中。这里 BX 将用于存储指向要进行交换操作的内存地址的指针。

  2. MOVL new+4(FP), AX:将栈帧指针 FP 加上偏移量 4 处的值(即参数 new)移动到寄存器 AX 中。这里 AX 存储着要与内存位置进行交换的新值。

  3. XCHGL AX, 0(BX):执行原子性的交换操作。将寄存器 AX 中的值与内存地址为 BX 指向的地址处的值进行交换。这里 BX 是指针,指向目标内存位置。

  4. MOVL AX, ret+8(FP):将寄存器 AX 的值(即交换操作后原始内存位置的值)移动到栈帧指针 FP 加上偏移量 8 处,可能是作为返回值的存储位置。

  5. RET:从函数返回。

Store 函数

TEXT ·Store(SB), NOSPLIT, $0-8
    MOVL   ptr+0(FP), BX
    MOVL   val+4(FP), AX
    XCHGL  AX, 0(BX)
    RET
  1. MOVL ptr+0(FP), BX:将栈帧指针 FP 加上偏移量 0 处的值(ptr 指向的值)移动到寄存器 BX 中。

  2. MOVL val+4(FP), AX:将 FP 加上偏移量 4 处的值(val的值)移动到寄存器 AX 中。

  3. XCHGL AX, 0(BX):交换指令,将寄存器 AX 中的值与内存地址为 BX + 0 处的值进行交换。这样就实现了将 AX 中的值存储到由 BX 指向的内存位置,同时原先内存中的值被加载到 AX 中。

  4. RET:从函数返回。

Load 函数

func Load(ptr *uint32) uint32 {
    return *ptr
}

Load 函数不是汇编实现的,但是真正执行时还会被编译成汇编后执行,底层是 MOV 指令:

func main() {
    var a int32 = 1
    b := atomic.LoadInt32(&a)
    fmt.Println(b)
}

& go build -gcflags="-N -l -S"  main.go
// ...
MOVD    main.&a-8(SP), R1   // 将 a 的地址保存到 R1 寄存器中
LDARW   (R1), R1            // 将 a 的值保存到 R1 寄存器中
MOVW    R1, main.b-68(SP)   // R1 寄存器中的值保存到 b 中

And 函数

Add 函数实现了一个按位与(And)操作。它接受一个指向 uint32 类型的指针 addr 和一个 uint32 类型的值 v,将 addr 所指向的内存位置的值与 v 进行按位与操作,并将结果存储回 addr 所指向的内存位置。

// func And(addr *uint32, v uint32)
TEXT ·And(SB), NOSPLIT, $0-8
    MOVL   ptr+0(FP), AX
    MOVL   val+4(FP), BX
    LOCK
    ANDL   BX, (AX)
    RET
  1. MOVL ptr+0(FP), AX:将栈帧指针 FP 加上偏移量 0 处的值(即参数 addr)移动到寄存器 AX 中。这里假设 AX 是一个 32 位寄存器。

  2. MOVL val+4(FP), BX:将栈帧指针 FP 加上偏移量 4 处的值(即参数 v)移动到寄存器 BX 中。

  3. LOCK:这是一个指令前缀,用于确保后续的指令执行是原子性的,防止在多处理器环境下出现并发访问问题。

  4. ANDL BX, (AX):对寄存器 BX 和 AX 所指向的内存位置的值进行按位与操作,并将结果存储回 AX 所指向的内存位置。这里,AX 中存储着参数 addr,即指向目标内存位置的指针,BX 中存储着参数 v。

  5. RET:从函数返回。

Or 函数

这个函数实现了一个按位或(Or)操作。它接受一个指向 uint32 类型的指针 addr 和一个 uint32 类型的值 v,将 addr 所指向的内存位置的值与 v 进行按位或操作,并将结果存储回 addr 所指向的内存位置。

// func Or(addr *uint32, v uint32)
TEXT ·Or(SB), NOSPLIT, $0-8
    MOVL   ptr+0(FP), AX
    MOVL   val+4(FP), BX
    LOCK
    ORL    BX, (AX)
    RET

  1. MOVL ptr+0(FP), AX:将栈帧指针 FP 加上偏移量 0 处的值(即参数 addr)移动到寄存器 AX 中。这里假设 AX 是一个 32 位寄存器。

  2. MOVL val+4(FP), BX:将栈帧指针 FP 加上偏移量 4 处的值(即参数 v)移动到寄存器 BX 中。

  3. LOCK:这是一个指令前缀,用于确保后续的指令执行是原子性的,防止在多处理器环境下出现并发访问问题。

  4. ORL BX, (AX):对寄存器 BX 和 AX 所指向的内存位置的值进行按位或操作,并将结果存储回 AX 所指向的内存位置。这里,AX 中存储着参数 addr,即指向目标内存位置的指针,BX 中存储着参数 v。

  5. RET:从函数返回。

原子类型

Atomic 除了提供了原子操作函数外,还提供了原子操作类型,但是这些原子类型底层还是基于原子操作函数实现的,这里不详细展开。

type Bool struct {
    _ noCopy
    v uint32
}

// Load atomically loads and returns the value stored in x.
func (x *Bool) Load() bool { return LoadUint32(&x.v) != 0 }

// Store atomically stores val into x.
func (x *Bool) Store(val bool) { StoreUint32(&x.v, b32(val)) }

// Swap atomically stores new into x and returns the previous value.
func (x *Bool) Swap(new bool) (old bool) { return SwapUint32(&x.v, b32(new)) != 0 }

// CompareAndSwap executes the compare-and-swap operation for the boolean value x.
func (x *Bool) CompareAndSwap(old, new bool) (swapped bool) {
    return CompareAndSwapUint32(&x.v, b32(old), b32(new))
}

小结

  1. Cas 函数:LOCK + CMPXCHGL 指令

  2. Xadd 函数:LOCK + ADD 指令

  3. Xchg 函数:LOCK + XCHGL 指令

  4. Store 函数:XCHGL 指令

  5. Load 函数: MOV 指令

  6. Add 函数:LOCK + AND 指令

  7. Or 函数:LOCK + OR 指令

源码中的原子操作

sync.Once

sync.Once 中使用 Load() 原子操作,判断是否执行过,但是需要注意的 Load() 是原子的,但是 if 判断语句并不是原子的,所以在 doSlow 里面还需要加锁。外面使用 Load() 是为了提高性能。

func (o *Once) Do(f func()) {
    if o.done.Load() == 0 {
       o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done.Load() == 0 {
       defer o.done.Store(1)
       f()
    }
}

sync.Mutex

sync.Mutex 使用 CompareAndSwapInt32 来判断有没有加锁, CompareAndSwapInt32 比 Load 功能更强大,不大能判断状态还能原子性的更变状态。

func (m *Mutex) Lock() {
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
       if race.Enabled {
          race.Acquire(unsafe.Pointer(m))
       }
       return
    }
    
    m.lockSlow()
}

sync.RWMutex

func (rw *RWMutex) RLock() {
    if race.Enabled {
       _ = rw.w.state
       race.Disable()
    }
    if rw.readerCount.Add(1) < 0 {
       // A writer is pending, wait for it.
       runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
    }
    if race.Enabled {
       race.Enable()
       race.Acquire(unsafe.Pointer(&rw.readerSem))
    }
}

在 Go 的同步原语的源码中有很多都是基于原子操作的,可以说原子操作是同步原语的基石。

任意类型的原子操作

前面所介绍的原子操作仅能对int32、int64、uint32、uint64、uintptr、unsafe.Pointer这些类型的值进行操作。然而在实际的开发过程中,我们所涉及的类型众多,例如string、struct等等,那么对于这些类型的值,应如何进行原子操作呢?答案是运用atomic.Value。

atomic.Value 支持以下操作:

  • Load:原子性的读取 Value 中的值。

  • Store:原子性的存储一个值到 Value 中。

  • Swap:原子性的交换 Value 中的值,返回旧值。

  • CompareAndSwap:原子性的比较并交换 Value 中的值,如果旧值和 old 相等,则将 new 存入 Value 中,返回 true,否则返回 false。

atomic.Value 是一个结构体,它的内部有一个 any 类型的字段,存储了我们要原子操作的值,也就是一个任意类型的值。

type Value struct {
    v any
}

在对 atomic.Value 进行原子操作时,会将 v any 转换为 efaceWords 类型。efaceWords 具有 typ 和 data 两个字段,它们都是 unsafe.Pointer 类型, unsafe.Pointer 类型是支持原子操作的,atomic.Value 的原理就是对 typ 和 data 两个字段分别作原子操作。

为什么可以转换参考 《深入 go interface 底层原理》 、《深入理解 go unsafe

type efaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}

func (v *Value) Load() (val any) {
    vp := (*efaceWords)(unsafe.Pointer(v))
    // ...
}
转载自:https://juejin.cn/post/7410222585211502643
评论
请登录