一文搞定 Go 原子操作

前言

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

原子操作的特点

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

  • 完整性:原子操作要么完全执行,要么完全不执行,不会出现执行到一半的情况。
  • 不可分割性:不能被其他操作打断,整个操作作为一个独立的、不可分割的单元执行。
  • 原子性:保证操作的原子性是为了确保数据的一致性和正确性。例如,在对一个共享变量进行修改时,如果这个修改操作不是原子的,可能会导致数据的不一致性。

原子操作的应用

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

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

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 包实现的,主要包括了 AddCompareAndSwapSwapLoadStore 等原子操作,go 1.23 还引入 AndOr 操作。

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 的伪代码如下:

  • 如果 *addrold 相等,则把 new 赋值给 *addr, 并返回 true,代表交换成功;
  • 如果不相等则直接返回 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
}

源码中的原子操作

  • 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 来判断有没有加锁, CompareAndSwapInt32Load 功能更强大,不大能判断状态还能原子性的更变状态。

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 的同步原语的源码中有很多都是基于原子操作的,可以说原子操作是同步原语的基石。

任意类型的原子操作

前面所介绍的原子操作仅能对int32int64uint32uint64uintptrunsafe.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 具有 typdata 两个字段,它们都是 unsafe.Pointer 类型, unsafe.Pointer 类型是支持原子操作的,atomic.Value 的原理就是对 typdata 两个字段分别作原子操作。

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

func (v *Value) Load() (val any) {
    vp := (*efaceWords)(unsafe.Pointer(v))
    // ...
}

文章来源: 一文搞定 Go 原子操作

关于我
loading