一文搞定 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 包实现的,主要包括了 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 的伪代码如下:
- 如果
*addr
与old
相等,则把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
来判断有没有加锁, 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
两个字段分别作原子操作。
type efaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
func (v *Value) Load() (val any) {
vp := (*efaceWords)(unsafe.Pointer(v))
// ...
}
文章来源: 一文搞定 Go 原子操作