首页 > 编程笔记 > Go语言笔记 阅读:29

Golang atomic原子操作的用法(附带实例)

比较并交换(Compare And Swap,CAS)是一种重要的同步原语,它在许多并发编程模型中起着核心作用。它用来比较给定值和内存地址中的值,如果它们是相同的值,就使用新值替换内存地址中的值,即使新值与旧值相同,这个操作也会执行。

虽然这看起来似乎没有意义,因为新值和旧值相同,替换后的值仍然和原来的值相同。但是,CAS 的关键在于它提供了一个原子性的检查和设值的操作,它是实现互斥锁和同步的基础。

什么是原子性?原子是化学反应中不可再分的最小粒子。引申到计算机领域,原子操作就是一个独立的操作单元,操作要么全部成功,要么全部失败。

原子操作可保证当前指令总是基于最新的值来计算。如果其他线程同时修改了这个值,那么 CAS 会返回失败的信息。在硬件层面,CPU 提供了原子操作、中断、内存总线和内存屏障等机制。原子操作确保了在并发环境中一组操作的完整性和一致性,中断可以暂停当前任务并切换到其他任务上,内存总线和内存屏障则确保了内存操作的顺序和一致性,这些机制是并发控制的基础。

操作系统基于这些机制实现了锁和同步机制,进而可以在用户空间提供并发支持。原子操作通常是用于实现锁的基本操作,如加锁和释放锁,而中断、内存总线和内存屏障则确保了并发操作的正确性。

原子操作在并发编程中非常重要,由于它是直接由 CPU 执行的,所以它的执行效率非常高,远高于锁和其他同步机制。

Go 语言中的 atomic 包为我们提供了一系列的原子操作函数,包括 Add(加)、CompareAndSwap(比较并交换)、Store(存储)、Load(加载)和Swap(交换)等。这些操作都是基于内存地址进行的,因此我们需要将可寻址变量的地址作为参数传递给这些函数。

Go语言中的原子操作

前面提到的互斥锁和读写锁,它们的底层都是通过组合封装原子操作来实现的。

在其源码中,随处可见调用原子操作的包 atomic 的代码,示例代码如下:
atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)
atomic.AddInt32(&m.state, delta)
既然互斥锁和读写锁是由一系列原子操作组合而成,那么它们肯定不能更改。

来看一个类比,如果将乐高积木里的每一个积木都看作一个原子操作,那么可以把互斥锁和读写锁理解为乐高官方组装好的系列套装,虽然我们可以用这个套装中的积木拼装出自己想要的其他样式,但是套装中的颗粒数和大小都是固定的、受限制的,其自由度比不了散装颗粒。不过,在有特殊需求时,我们可以像高级玩家一样,使用散装的颗粒(原子操作)来拼装出我们想要的样式(编写出我们想要实现的锁)。

不仅互斥锁需要用到原子操作,Go 语言中重要的 GPM 调度也少不了原子操作的身影。在与调度有关的源码 runtime/proc.go 中,可以看到调度器一个很重要的操作就是从本地运行队列中获取协程,与之相关的 runqget 函数如下:
func runqget(_p_ *p) (gp *g, inheritTime bool)
在函数 runqget() 中,调用了原子读取函数 atomic.LoadAcq 以及原子比较交换函数 atomic.CasRel,它们都是典型的原子操作。

Go语言atomic包的使用

下面将数据竞争章节涉及的代码使用 atomic 包重构,以使其并发安全,示例代码如下:
func TestGoroutineSafeByAtomic(t *testing.T) {
    var counter int64 = 0
    for i := 0; i < 100000; i++ {
        go func() {
            atomic.AddInt64(&counter, 1)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter)
}
使用 atomic 包时,会强制要求加上数据类型的精度。因此,必须明确要使用的是带长度的 int 类型(如 int32、int64),而不是不带长度的 int 类型,这也是使用这套 API 的一个限制。

使用 atomic 包时,需要将地址作为第一个参数传入。这些同步操作针对的是某个地址对应的值。这种同步方式依赖于地址,硬件会保证多个协程有序地访问同一个内存地址。

要说明的是,atomic 包仅仅会为一行代码保证原子性。如果需要让一段代码都保持原子性,还是得使用互斥锁,这就是使用这套 API 的第二个限制。

另外,atomic 包中的结构体 atomic.Value 是一个存、取均为原子操作的容器,它不支持 CAS 操作,常用于变更配置的场景。关键代码如下:
var atomicVal atomic.Value
str := "hello"
 
atomicVal.Store(str) //此处是原子操作
newStr := atomicVal.Load() //此处是原子操作

相关文章