概述
为了保证并发安全,除了使用临界区(如互斥锁)之外,还可以使用原子操作。原子操作指的是在执行过程中不会被中断的操作,这样可以确保在多线程环境下的数据操作的一致性。Go 语言中的 atomic 包提供了一系列原子操作,用于对基础数据类型进行安全的并发操作。
atomic 包支持以下几种基础类型的原子操作:int32、int64、uint32、uint64、uintptr、unsafe.Pointer。对于每一种类型,提供了以下几类原子操作:
Add:增加或减少值CompareAndSwap:比较并交换值Swap:交换值Load:读取值Store:存储值
函数名通常由原子操作名和类型关键字组成,例如 AddInt32 是对 int32 类型进行的加法操作。本文以 int32 类型为例进行说明,其他类型的操作类似。
AddInt32
AddInt32 函数用于对 int32 类型的变量进行原子增加或减少操作。函数签名如下:
func AddInt32(addr *int32, delta int32) (new int32)
该函数接收两个参数,一个是要修改的变量的地址,另一个是要增加或减少的值(delta)。该函数会直接修改传入的变量,并返回修改后的新值。
- 若
delta为正数,则进行加法操作。 - 若
delta为负数,则进行减法操作。
需要注意的是,当处理 uint32 和 uint64 类型时,由于 delta 参数类型被限定为无符号整数,不能直接传递负数,这时需要利用二进制补码机制来实现减法操作。
示例代码
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var a int32 = 10
atomic.AddInt32(&a, 10)
fmt.Println(a == 20) // true
var b uint32 = 20
atomic.AddUint32(&b, ^uint32(10-1))
// 等价于 b -= 10
fmt.Println(b == 10) // true
}
在这个示例中,AddInt32 函数成功将 a 增加了 10,而 AddUint32 则通过补码机制减少了 b 的值。
CompareAndSwapInt32
CompareAndSwapInt32 函数用于比较并交换 int32 类型的值。函数签名如下:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
该函数接收三个参数:一个是要修改的变量地址,另一个是要比较的旧值,最后是要交换的新值。函数返回一个布尔值,表示交换是否成功。
CompareAndSwap 操作的原理是先比较变量当前的值和旧值是否相等,如果相等,则将变量值替换为新值,否则不进行替换。
SwapInt32
SwapInt32 函数用于直接交换 int32 类型的值,并返回旧值。函数签名如下:
func SwapInt32(addr *int32, new int32) (old int32)
与 CompareAndSwap 不同,Swap 不进行比较,直接执行赋值操作,并返回交换前的旧值。
LoadInt32 & StoreInt32
LoadInt32 和 StoreInt32 分别用于原子地读取和存储 int32 类型的值。这两个操作在并发环境中非常重要,确保了变量的读取和写入过程不会被中断。
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
LoadInt32接收一个变量地址并返回该变量的当前值。StoreInt32接收一个变量地址和一个新值,直接将新值写入该变量。
示例代码
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var a int32 = 10
val := atomic.LoadInt32(&a)
fmt.Println(val == 10) // true
atomic.StoreInt32(&a, 20)
fmt.Println(a == 20) // true
}
在这个示例中,LoadInt32 函数读取了变量 a 的值,而 StoreInt32 函数则将新值 20 写入了变量 a。
通过 atomic 包中的这些原子操作,Go 语言能够有效地处理多线程环境下的数据一致性问题,避免了使用互斥锁可能带来的性能开销。合理地使用这些原子操作可以编写出更高效、安全的并发程序。