全球即时:什么是原子操作?深入浅析go中的原子操作
在我们前面的一些介绍 sync包相关的文章中,我们应该也发现了,其中有不少地方使用了原子操作。比如 sync.WaitGroup、sync.Map再到 sync.Pool,这些结构体的实现中都有原子操作的身影。原子操作在并发编程中是一种非常重要的操作,它可以保证并发安全,而且效率也很高。本文将会深入探讨一下 go 中原子操作的原理、使用场景、用法等内容。
什么是原子操作?
如果让我用一句话来说明什么是原子操作,那就是:原子操作是变量级别的互斥锁。简单来说,就是同一时刻,只能有一个 CPU 对变量进行读或写。当我们想要对某个变量做并发安全的修改,除了使用官方提供的 Mutex,还可以使用 sync/atomic包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。
【资料图】
我们可以用下图来表示:
说明:在上图中,我们有三个 CPU 逻辑核,其中 CPU 1 正在对变量 v做原子操作,这个时候 CPU 2 和 CPU 3 不能对 v做任何操作,在 CPU 1 操作完成后,CPU 2 和 CPU 3 可以获取到 v的最新值。
从这个角度看,我们可以把 sync/atomic包中的原子操作看成是变量级别的互斥锁。就是说,在 go 中,当一个协程对变量做原子操作时,其他协程不能对这个变量做任何操作,直到这个协程操作完成。
原子操作的使用场景是什么?
拿一个简单的例子来说明一下原子操作的使用场景:
func TestAtomic(t *testing.T) {var sum = 0var wg sync.WaitGroupwg.Add(1000)// 启动 1000 个协程,每个协程对 sum 做加法操作for i := 0; i < 1000; i++ {go func() {defer wg.Done()sum++}()}// 等待所有的协程都执行完毕wg.Wait()fmt.Println(sum) // 这里输出多少呢?}我们可以在自己的电脑上运行一下这段代码,看看输出的结果是多少。不出意外的话,应该每次可能都不一样,而且应该也不是 1000,这是为什么呢?
这是因为,CPU 在对 sum做加法的时候,需要先将 sum目前的值读取到 CPU 的寄存器中,然后再进行加法操作,最后再写回到内存中。如果有两个 CPU 同时取了 sum的值,然后都进行了加法操作,然后都再写回到内存中,那么就会导致 sum的值被覆盖,从而导致结果不正确。
举个例子,目前内存中的 sum为 1,然后两个 CPU 同时取了这个 1 来做加法,然后都得到了结果 2,然后这两个 CPU 将各自的计算结果写回到内存中,那么内存中的 sum就变成了 2,而不是 3。
在这种场景下,我们可以使用原子操作来实现并发安全的加法操作:
func TestAtomic1(t *testing.T) {// 将 sum 的类型改成 int32,因为原子操作只能针对 int32、int64、uint32、uint64、uintptr 这几种类型var sum int32 = 0var wg sync.WaitGroupwg.Add(1000) // 启动 1000 个协程,每个协程对 sum 做加法操作for i := 0; i < 1000; i++ {go func() {defer wg.Done()// 将 sum++ 改成下面这样atomic.AddInt32(&sum, 1)}()}wg.Wait()fmt.Println(sum) // 输出 1000}在上面这个例子中,我们每次执行都能得到 1000 这个结果。
因为使用原子操作的时候,同一时刻只能有一个 CPU 对变量进行读或写,所以就不会出现上面的问题了。
所以很多需要对变量做并发读写的地方,我们都可以考虑一下,是否可以使用原子操作来实现并发安全的操作(而不是使用互斥锁,互斥锁效率相比原子操作要低一些)。
原子操作的使用场景也是和互斥锁类似的,但是不一样的是,我们的锁粒度只是一个变量而已。也就是说,当我们不允许多个 CPU 同时对变量进行读写的时候(保证变量同一时刻只能一个 CPU 操作),就可以使用原子操作。
原子操作是怎么实现的?
看完上面原子操作的介绍,有没有觉得原子操作很神奇,居然有这么好用的东西。那它到底是怎么实现的呢?
一般情况下,原子操作的实现需要特殊的 CPU 指令或者系统调用。这些指令或者系统调用可以保证在执行期间不会被其他操作或事件中断,从而保证操作的原子性。
例如,在 x86 架构的 CPU 中,可以使用 LOCK前缀来实现原子操作。LOCK前缀可以与其他指令一起使用,用于锁定内存总线,防止其他 CPU 访问同一内存地址,从而实现原子操作。在使用 LOCK前缀的指令执行期间,CPU 会将当前处理器缓存中的数据写回到内存中,并锁定该内存地址,防止其他 CPU 修改该地址的数据(所以原子操作总是可以读取到最新的数据)。一旦当前 CPU 对该地址的操作完成,CPU 会释放该内存地址的锁定,其他 CPU 才能继续对该地址进行访问。
x86 LOCK 的时候发生了什么
我们再来捋一下上面的内容,看看 LOCK前缀是如何实现原子操作的:
其他架构的 CPU 可能会略有不同,但是原理是一样的。
原子操作有什么特征?
不会被中断:原子操作是一个不可分割的操作,要么全部执行,要么全部不执行,不会出现中间状态。这是保证原子性的基本前提。同时,原子操作过程中不会有上下文切换的过程。操作对象是共享变量:原子操作通常是对共享变量进行的,也就是说,多个协程可以同时访问这个变量,因此需要采用原子操作来保证数据的一致性和正确性。并发安全:原子操作是并发安全的,可以保证多个协程同时进行操作时不会出现数据竞争问题(虽然说是同时,但是实际上在操作那个变量的时候是互斥的)。无需加锁:原子操作不需要使用互斥锁来保证数据的一致性和正确性,因此可以避免互斥锁的使用带来的性能损失。适用场景比较局限:原子操作适用于操作单个变量,如果需要同时并发读写多个变量,可能需要考虑使用互斥锁。go 里面有哪些原子操作?
在 go 中,主要有以下几种原子操作:Add、CompareAndSwap、Load、Store、Swap。
增减(Add)
用于进行增加或减少的原子操作,函数名以Add为前缀,后缀针对特定类型的名称。原子增被操作的类型只能是数值类型,即 int32、int64、uint32、uint64、uintptr原子增减函数的第一个参数为原值,第二个参数是要增减多少。方法: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)
int32和 int64的第二个参数可以是负数,这样就可以做原子减法了。
比较并交换(CompareAndSwap)
也就是我们常见的 CAS,在 CAS操作中,会需要拿旧的值跟 old比较,如果相等,就将 new赋值给 addr。如果不相等,则不做任何操作。最后返回一个 bool值,表示是否成功 swap。
也就是说,这个操作可能是不成功的。这很正常,在并发环境下,多个协程对同一个变量进行操作,肯定会存在竞争的情况。在这种情况下,偶尔的失败是正常的,我们只需要在失败的时候,重新尝试即可。因为原子操作需要的时间往往是比较短的,因此在失败的时候,我们可以通过自旋的方式来再次进行尝试。
在这种情况下,如果不自旋,那就需要将这个协程挂起,等待其他协程完成操作,然后再次尝试。这个过程相比自旋可能会更加耗时。因为很有可能这次原子操作不成功,下一次就成功了。如果我们每次都将协程挂起,那么效率就会大大降低。
for+ 原子操作的方式,在 go 的 sync包中很多地方都有使用,比如 sync.Map,sync.Pool等。这也是使用原子操作时一个非常常见的使用模式。
CompareAndSwap的功能:
CompareAndSwap为前缀,后缀针对特定类型的名称。原子比较并交换被操作的类型可以是数值类型或指针类型,即 int32、int64、uint32、uint64、uintptr、unsafe.Pointer原子比较并交换函数的第一个参数为原值指针,第二个参数是要比较的值,第三个参数是要交换的值。方法:func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)func CompareAndSwapInt64(addr *int64, old, new int64) (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)func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
载入(Load)
原子性的读取操作接受一个对应类型的指针值,返回该指针指向的值。原子性读取意味着读取值的同时,当前计算机的任何 CPU 都不会进行针对值的读写操作。
如果不使用原子 Load,当使用 v := value这种赋值方式为变量 v赋值时,读取到的 value可能不是最新的,因为在读取操作时其他协程对它的读写操作可能会同时发生。
Load 操作有下面这些:
func LoadInt32(addr *int32) (val int32)func LoadInt64(addr *int64) (val int64)func LoadUint32(addr *uint32) (val uint32)func LoadUint64(addr *uint64) (val uint64)func LoadUintptr(addr *uintptr) (val uintptr)func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
存储(Store)
Store可以将 val值保存到 *addr中,Store操作是原子性的,因此在执行 Store操作时,当前计算机的任何 CPU 都不会进行针对 *addr的读写操作。
val值保存到 *addr中。与读操作对应的写入操作,sync/atomic提供了与原子值载入 Load函数相对应的原子值存储 Store函数,原子性存储函数均以 Store为前缀。Store操作有下面这些:
func StoreInt32(addr *int32, val int32)func StoreInt64(addr *int64, val int64)func StoreUint32(addr *uint32, val uint32)func StoreUint64(addr *uint64, val uint64)func StoreUintptr(addr *uintpre, val uintptr)func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
交换(Swap)
Swap跟 Store有点类似,但是它会返回 *addr的旧值。
func SwapInt32(addr *int32, new int32) (old int32)func SwapInt64(addr *int64, new int64) (old int64)func SwapUint32(addr *uint32, new uint32) (old uint32)func SwapUint64(addr *uint64, new uint64) (old uint64)func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
原子操作任意类型的值 - atomic.Value
从上一节中,我们知道了在 go 中原子操作可以操作 int32、int64、uint32、uint64、uintptr、unsafe.Pointer这些类型的值。但是在实际开发中,我们的类型还有很多,比如 string、struct等等,那这些类型的值如何进行原子操作呢?答案是使用 atomic.Value。
atomic.Value是一个结构体,它的内部有一个 any类型的字段,存储了我们要原子操作的值,也就是一个任意类型的值。
atomic.Value支持以下操作:
Load:原子性的读取 Value中的值。Store:原子性的存储一个值到 Value中。Swap:原子性的交换 Value中的值,返回旧值。CompareAndSwap:原子性的比较并交换 Value中的值,如果旧值和 old相等,则将 new存入 Value中,返回 true,否则返回 false。atomic.Value的这些操作跟上面讲到的那些操作其实差不多,只不过 atomic.Value可以操作任意类型的值。那 atomic.Value是如何实现的呢?
atomic.Value 源码分析
atomic.Value是一个结构体,这个结构体只有一个字段:
// Value 提供一致类型值的原子加载和存储。type Value struct {v any}Load - 读取
Load返回由最近的 Store设置的值。如果还没有 Store过任何值,则返回 nil。
// Load 返回由最近的 Store 设置的值。func (v *Value) Load() (val any) {// atomic.Value 转换为 efaceWordsvp := (*efaceWords)(unsafe.Pointer(v))// 判断 atomic.Value 的类型typ := LoadPointer(&vp.typ)// 第一次 Store 还没有完成,直接返回 nilif typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {// firstStoreInProgress 是一个特殊的变量,存储到 typ 中用来表示第一次 Store 还没有完成return nil}// 获取 atomic.Value 的值data := LoadPointer(&vp.data)// 将 val 转换为 efaceWords 类型vlp := (*efaceWords)(unsafe.Pointer(&val))// 分别赋值给 val 的 typ 和 datavlp.typ = typvlp.data = datareturn}在 atomic.Value的源码中,我们都可以看到 efaceWords的身影,它实际上代表的是 interface{}/any类型:
// 表示一个 interface{}/any 类型type efaceWords struct {typ unsafe.Pointerdata unsafe.Pointer}看到这里我们会不会觉得很困惑,直接返回 val不就可以了吗?为什么要将 val转换为 efaceWords类型呢?
这是因为 go 中的原子操作只能操作 int32、int64、uint32、uint64、uintptr、unsafe.Pointer这些类型的值,不支持 interface{}类型,但是如果了解 interface{}底层结构的话,我们就知道 interface{}底层其实就是一个结构体,它有两个字段,一个是 type,一个是 data,type用来存储 interface{}的类型,data用来存储 interface{}的值。而且这两个字段都是 unsafe.Pointer类型的,所以其实我们可以对 interface{}的 type和 data分别进行原子操作,这样最终其实也可以达到了原子操作 interface{}的目的了,是不是非常地巧妙呢?
Store - 存储
Store将 Value的值设置为 val。对给定值的所有存储调用必须使用相同具体类型的值。不一致类型的存储会发生恐慌,Store(nil)也会 panic。
// Store 将 Value 的值设置为 val。func (v *Value) Store(val any) {// 不能存储 nil 值if val == nil {panic("sync/atomic: store of nil value into Value")}// atomic.Value 转换为 efaceWordsvp := (*efaceWords)(unsafe.Pointer(v))// val 转换为 efaceWordsvlp := (*efaceWords)(unsafe.Pointer(&val))// 自旋进行原子操作,这个过程不会很久,开销相比互斥锁小for {// LoadPointer 可以保证获取到的是最新的typ := LoadPointer(&vp.typ)// 第一次 store 的时候 typ 还是 nil,说明是第一次 storeif typ == nil {// 尝试开始第一次 Store。// 禁用抢占,以便其他 goroutines 可以自旋等待完成。// (如果允许抢占,那么其他 goroutine 自旋等待的时间可能会比较长,因为可能会需要进行协程调度。)runtime_procPin()// 抢占失败,意味着有其他 goroutine 成功 store 了,允许抢占,再次尝试 Store// 这也是一个原子操作。if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {runtime_procUnpin()continue}// 完成第一次 store// 因为有 firstStoreInProgress 标识的保护,所以下面的两个原子操作是安全的。StorePointer(&vp.data, vlp.data) // 存储值(原子操作)StorePointer(&vp.typ, vlp.typ) // 存储类型(原子操作)runtime_procUnpin() // 允许抢占return}// 另外一个 goroutine 正在进行第一次 Store。自旋等待。if typ == unsafe.Pointer(&firstStoreInProgress) {continue}// 第一次 Store 已经完成了,下面不是第一次 Store 了。// 需要检查当前 Store 的类型跟第一次 Store 的类型是否一致,不一致就 panic。if typ != vlp.typ {panic("sync/atomic: store of inconsistently typed value into Value")}// 后续的 Store 只需要 Store 值部分就可以了。// 因为 atomic.Value 只能保存一种类型的值。StorePointer(&vp.data, vlp.data)return}}在 Store中,有以下几个注意的点:
firstStoreInProgress来确保第一次 Store的时候,只有一个 goroutine可以进行 Store操作,其他的 goroutine需要自旋等待。如果没有这个保护,那么存储 typ和 data的时候就会出现竞争(因为需要两个原子操作),导致数据不一致。在这里其实可以将 firstStoreInProgress看作是一个互斥锁。在进行第一次 Store的时候,会将当前的 goroutine 和 P绑定,这样拿到 firstStoreInProgress锁的协程就可以尽快地完成第一次 Store操作,这样一来,其他的协程也不用等待太久。在第一次 Store的时候,会有两个原子操作,分别存储类型和值,但是因为有 firstStoreInProgress的保护,所以这两个原子操作本质上是对 interface{}的一个原子存储操作。其他协程在看到有 firstStoreInProgress标识的时候,就会自旋等待,直到第一次 Store完成。在后续的 Store操作中,只需要存储值就可以了,因为 atomic.Value只能保存一种类型的值。Swap - 交换
Swap将 Value的值设置为 new并返回旧值。对给定值的所有交换调用必须使用相同具体类型的值。同时,不一致类型的交换会发生恐慌,Swap(nil)也会 panic。
// Swap 将 Value 的值设置为 new 并返回旧值。func (v *Value) Swap(new any) (old any) {// 不能存储 nil 值if new == nil {panic("sync/atomic: swap of nil value into Value")}// atomic.Value 转换为 efaceWordsvp := (*efaceWords)(unsafe.Pointer(v))// new 转换为 efaceWordsnp := (*efaceWords)(unsafe.Pointer(&new))// 自旋进行原子操作,这个过程不会很久,开销相比互斥锁小for {// 下面这部分代码跟 Store 一样,不细说了。// 这部分代码是进行第一次存储的代码。typ := LoadPointer(&vp.typ)if typ == nil {runtime_procPin()if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {runtime_procUnpin()continue}StorePointer(&vp.data, np.data)StorePointer(&vp.typ, np.typ)runtime_procUnpin()return nil}if typ == unsafe.Pointer(&firstStoreInProgress) {continue}if typ != np.typ {panic("sync/atomic: swap of inconsistently typed value into Value")}// ---- 下面是 Swap 的特有逻辑 ----// op 是返回值op := (*efaceWords)(unsafe.Pointer(&old))// 返回旧的值op.typ, op.data = np.typ, SwapPointer(&vp.data, np.data)return old}}CompareAndSwap - 比较并交换
CompareAndSwap将 Value的值与 old比较,如果相等则设置为 new并返回 true,否则返回 false。对给定值的所有比较和交换调用必须使用相同具体类型的值。同时,不一致类型的比较和交换会发生恐慌,CompareAndSwap(nil, nil)也会 panic。
// CompareAndSwap 比较并交换。func (v *Value) CompareAndSwap(old, new any) (swapped bool) {// 注意:old 是可以为 nil 的,new 不能为 nil。// old 是 nil 表示是第一次进行 Store 操作。if new == nil {panic("sync/atomic: compare and swap of nil value into Value")}// atomic.Value 转换为 efaceWordsvp := (*efaceWords)(unsafe.Pointer(v))// new 转换为 efaceWordsnp := (*efaceWords)(unsafe.Pointer(&new))// old 转换为 efaceWordsop := (*efaceWords)(unsafe.Pointer(&old))// old 和 new 类型必须一致,且不能为 nilif op.typ != nil && np.typ != op.typ {panic("sync/atomic: compare and swap of inconsistently typed values")}// 自旋进行原子操作,这个过程不会很久,开销相比互斥锁小for {// LoadPointer 可以保证获取到的 typ 是最新的typ := LoadPointer(&vp.typ)if typ == nil { // atomic.Value 是 nil,还没 Store 过// 准备进行第一次 Store,但是传递进来的 old 不是 nil,compare 这一步就失败了。直接返回 falseif old != nil {return false}// 下面这部分代码跟 Store 一样,不细说了。 // 这部分代码是进行第一次存储的代码。runtime_procPin()if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {runtime_procUnpin()continue}StorePointer(&vp.data, np.data)StorePointer(&vp.typ, np.typ)runtime_procUnpin()return true}if typ == unsafe.Pointer(&firstStoreInProgress) {continue}if typ != np.typ {panic("sync/atomic: compare and swap of inconsistently typed value into Value")}// 通过运行时相等性检查比较旧版本和当前版本。// 这允许对值类型进行比较,这是包函数所没有的。// 下面的 CompareAndSwapPointer 仅确保 vp.data 自 LoadPointer 以来没有更改。data := LoadPointer(&vp.data)var i any(*efaceWords)(unsafe.Pointer(&i)).typ = typ(*efaceWords)(unsafe.Pointer(&i)).data = dataif i != old { // atomic.Value 跟 old 不相等return false}// 只做 val 部分的 cas 操作return CompareAndSwapPointer(&vp.data, data, np.data)}}这里需要特别说明的只有最后那个比较相等的判断,也就是 data := LoadPointer(&vp.data)以及往后的几行代码。在开发 atomic.Value第一版的时候,那个开发者其实是将这几行写成 CompareAndSwapPointer(&vp.data, old.data, np.data)这种形式的。但是在旧的写法中,会存在一个问题,如果我们做 CAS操作的时候,如果传递的参数 old是一个结构体的值这种类型,那么这个结构体的值是会被拷贝一份的,同时再会被转换为 interface{}/any类型,这个过程中,其实参数的 old的 data部分指针指向的内存跟 vp.data指向的内存是不一样的。这样的话,CAS操作就会失败,这个时候就会返回 false,但是我们本意是要比较它的值,出现这种结果显然不是我们想要的。
将值作为 interface{}参数使用的时候,会存在一个将值转换为 interface{}的过程。具体我们可以看看 interface{}的实现原理。
所以,在上面的实现中,会将旧值的 typ和 data赋值给一个 any类型的变量,然后使用 i != old这种方式进行判断,这样就可以实现在比较的时候,比较的是值,而不是由值转换为 interface{}后的指针。
其他原子类型
我们现在知道了,atomic.Value可以对任意类型做原子操作。而对于其他的原子类型,比如 int32、int64、uint32、uint64、uintptr、unsafe.Pointer等,其实在 go 中也提供了包装的类型,让我们可以以对象的方式来操作这些类型。
对应的类型如下:
atomic.Bool:这个比较特别,但底层实际上是一个 uint32类型的值。我们对 atomic.Bool做原子操作的时候,实际上是对 uint32做原子操作。atomic.Int32:int32类型的包装类型atomic.Int64:int64类型的包装类型atomic.Uint32:uint32类型的包装类型atomic.Uint64:uint64类型的包装类型atomic.Uintptr:uintptr类型的包装类型atomic.Pointer:unsafe.Pointer类型的包装类型这几种类型的实现的代码基本一样,除了类型不一样,我们可以看看 atomic.Int32的实现:
// An Int32 is an atomic int32. The zero value is zero.type Int32 struct {_ noCopyv int32}// Load atomically loads and returns the value stored in x.func (x *Int32) Load() int32 { return LoadInt32(&x.v) }// Store atomically stores val into x.func (x *Int32) Store(val int32) { StoreInt32(&x.v, val) }// Swap atomically stores new into x and returns the previous value.func (x *Int32) Swap(new int32) (old int32) { return SwapInt32(&x.v, new) }// CompareAndSwap executes the compare-and-swap operation for x.func (x *Int32) CompareAndSwap(old, new int32) (swapped bool) {return CompareAndSwapInt32(&x.v, old, new)}可以看到,atomic.Int32的实现都是基于 atomic包中 int32类型相关的原子操作函数来实现的。
原子操作与互斥锁比较
那我们有了互斥锁,为什么还要有原子操作呢?我们进行比较一下就知道了:
| 原子操作 | 互斥锁 | |
|---|---|---|
| 保护的范围 | 变量 | 代码块 |
| 保护的粒度 | 小 | 大 |
| 性能 | 高 | 低 |
| 如何实现的 | 硬件指令 | 软件层面实现,逻辑较多 |
如果我们只需要对某一个变量做并发读写,那么使用原子操作就可以了,因为原子操作的性能比互斥锁高很多。但是如果我们需要对多个变量做并发读写,那么就需要用到互斥锁了,这种场景往往是在一段代码中对不同变量做读写。
性能比较
我们前面这个表格提到了原子操作与互斥锁性能上有差异,我们写几行代码来进行比较一下:
// 系统信息 cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz// 10.13 ns/opfunc BenchmarkMutex(b *testing.B) { var mu sync.Mutex for i := 0; i < b.N; i++ { mu.Lock() mu.Unlock() }}// 5.849 ns/opfunc BenchmarkAtomic(b *testing.B) { var sum atomic.Uint64 for i := 0; i < b.N; i++ { sum.Add(uint64(1)) }}在对 Mutex的性能测试中,我只是写了简单的 Lock()和 UnLock()操作,因为这种比较才算是对 Mutex本身的测试,而在 Atomic的性能测试中,对 sum做原子累加的操作。最终结果是,使用 Atomic的操作耗时大概比 Mutex少了 40%以上。
在实际开发中,Mutex保护的临界区内往往有更多操作,也就意味着 Mutex锁需要耗费更长的时间才能释放,也就是会需要耗费比上面这个 40%还要多的时间另外一个协程才能获取到 Mutex锁。
go 的 sync 包中的原子操作
在文章的开头,我们就说了,在 go 的 sync.Map和 sync.Pool中都有用到了原子操作,本节就来看一看这些操作。
sync.Map 中的原子操作
在 sync.Map中使用到了一个 entry结构体,这个结构体中大部分操作都是原子操作,我们可以看看它下面这两个方法的定义:
// 删除 entryfunc (e *entry) delete() (value any, ok bool) {for {p := e.p.Load()// 已经被删除了,不需要再删除if p == nil || p == expunged {return nil, false}// 删除成功if e.p.CompareAndSwap(p, nil) {return *p, true}}}// 如果条目尚未删除,trySwap 将交换一个值。func (e *entry) trySwap(i *any) (*any, bool) {for {p := e.p.Load()// 已经被删除了if p == expunged {return nil, false}// swap 成功if e.p.CompareAndSwap(p, i) {return p, true}}}我们可以看到一个非常典型的特征就是 for+ CompareAndSwap的组合,这个组合在 entry中出现了很多次。
如果我们也需要对变量做并发读写,也可以尝试一下这种 for + CompareAndSwap 的组合。
sync.WaitGroup 中的原子操作
在 sync.WaitGroup中有一个类型为 atomic.Uint64的 state字段,这个变量是用来记录 WaitGroup的状态的。在实际使用中,它的高 32 位用来记录 WaitGroup的计数器,低 32 位用来记录 WaitGroup的 Waiter的数量,也就是等待条件变量满足的协程数量。
如果不使用一个变量来记录这两个值,那么我们就需要使用两个变量来记录,这样就会导致我们需要对两个变量做并发读写,在这种情况下,我们就需要使用互斥锁来保护这两个变量,这样就会导致性能的下降。
而使用一个变量来记录这两个值,我们就可以使用原子操作来保护这个变量,这样就可以保证并发读写的安全性,同时也能得到更好的性能:
// WaitGroup 的 Add 函数:高 32 位加上 deltastate := wg.state.Add(uint64(delta) << 32)// WaitGroup 的 Wait 函数:低 32 位加 1// 等待者的数量加 1wg.state.CompareAndSwap(state, state+1)
CAS 操作有失败必然有成功
当然这里是指指向同一行 CAS代码的时候(也就是有竞争的时候),如果是指向不同行 CAS代码的时候,那么就不一定了。比如下面这个例子,我们把前面计算 sum的例子改一改,改成用 CAS操作来完成:
func TestCas(t *testing.T) {var sum int32 = 0var wg sync.WaitGroupwg.Add(1000)for i := 0; i < 1000; i++ {go func() {defer wg.Done()// 这一行是有可能会失败的atomic.CompareAndSwapInt32(&sum, sum, sum+1)}()}wg.Wait()fmt.Println(sum) // 不是 1000}在这个例子中,我们把 atomic.AddInt32(&sum, 1)改成了 atomic.CompareAndSwapInt32(&sum, sum, sum+1),这样就会导致有可能会有多个 goroutine 同时执行到 atomic.CompareAndSwapInt32(&sum, sum, sum+1)这一行代码,这样肯定会有不同的 goroutine 同时拿到一个相同的 sum的旧值,那么在这种情况下,就会导致 CAS操作失败。也就是说,将 sum替换为 sum + 1的操作可能会失败。
失败意味着什么呢?意味着另外一个协程序先把 sum的值加 1 了,这个时候其实我们不应该在旧的 sum上加 1 了,而是应该在最新的 sum上加上 1,那我们应该怎么做呢?我们可以在 CAS操作失败的时候,重新获取 sum的值,然后再次尝试 CAS操作,直到成功为止:
func TestCas(t *testing.T) {var sum int32 = 0var wg sync.WaitGroupwg.Add(1000)for i := 0; i < 1000; i++ {go func() {defer wg.Done()// cas 失败的时候,重新获取 sum 的值进行计算。// cas 成功则返回。for {if atomic.CompareAndSwapInt32(&sum, sum, sum+1) {return}}}()}wg.Wait()fmt.Println(sum)}总结
原子操作是并发编程中非常重要的一个概念,它可以保证并发读写的安全性,同时也能得到更好的性能。
最后,总结一下本文讲到的内容:
原子操作是更加底层的操作,它保护的是单个变量,而互斥锁可以保护一个代码片段,它们的使用场景是不一样的。原子操作需要通过 CPU 指令来实现,而互斥锁是在软件层面实现的。go 里面的原子操作有以下这些:Add:原子增减CompareAndSwap:原子比较并交换Load:原子读取Store:原子写入Swap:原子交换go 里面所有类型都能使用原子操作,只是不同类型的原子操作使用的函数不太一样。atomic.Value可以用来原子操作任意类型的变量。go 里面有些底层实现也使用了原子操作,比如:sync.WaitGroup:使用原子操作来保证计数器和等待者数量的并发读写安全性。sync.Map:entry结构体中基本所有操作都有原子操作的身影。原子操作有失败必然有成功(说的是同一行 CAS操作),如果 CAS操作失败了,那么我们可以重新获取旧值,然后再次尝试 CAS操作,直到成功为止。总的来说,原子操作本身其实没有太复杂的逻辑,我们理解了它的原理之后,就可以很容易的使用它了。
推荐学习:Golang教程
以上就是什么是原子操作?深入浅析go中的原子操作的详细内容,更多请关注php中文网其它相关文章!
标签:
-
2022-02-07 14:57:45
奇迹!绝杀!女足亚洲杯逆转夺冠!<
刚刚,中国女足上演逆转绝杀奇迹!她们在亚洲杯决赛中3:2力克韩国队,时隔16年再夺亚洲杯冠军!
-
2022-02-07 14:57:45
中国政府与阿根廷共和国政府签署共建“一带一路”谅解备忘录<
新华社北京2月6日电(记者安蓓)国家发展改革委6日称,国家发展改革委主任何立峰与阿根廷外交、国际贸易和宗教事
-
2022-02-07 14:57:43
中华人民共和国和阿根廷共和国关于深化中阿全面战略伙伴关系的联合声明(全文)<
新华社北京2月6日电中华人民共和国和阿根廷共和国关于深化中阿全面战略伙伴关系的联合声明一、应中方邀请,阿根廷
-
2022-02-07 14:57:40
春节假期国内旅游出游2.51亿人次<
春节遇冬奥,旅游年味浓。根据文化和旅游部数据中心测算,2022年春节假期7天,全国国内旅游出游2 51亿人次,同比
-
2022-02-07 14:57:40
中吉签署关于经典著作互译出版的备忘录 开启两国人文交流互鉴新阶段<
新华社北京2月6日电(记者史竞男)国家主席习近平6日会见来华出席北京2022年冬奥会开幕式的吉尔吉斯斯坦总统扎帕
-
2023-03-28 19:00:42
全球即时:什么是原子操作?深入浅析go中的原子操作
在我们前面的一些介绍sync包相关的文章中,我们应该也发现了,其中有不少地方使用了原子操作。今天让我们来深入探讨一下go中的原子操作原理、
-
2023-03-28 18:30:01
全球快播:市场监管总局:去年质量技术帮扶企业超过5.45万家
北京商报讯(记者方彬楠张晗)3月28日,在市场监管总局召开的新闻发布会上,市场监管总局新闻发言人于军表示,《市场监管总局办公厅关于开展质量
-
2023-03-28 17:20:10
【焦点热闻】煤铝价格齐涨,年报业绩亮眼
第一时间提供各大券商研究所报告,最大程度减少个人投资者与机构之间信息上的差异,使个人投资者更早的了解到上市公司基本面变化。
-
2023-03-28 16:31:55
兴达路街道开展“健康守护 预防先行”流感知识讲座-全球资讯
春季是流感高发时期,为了让小朋友们深入了解流感知识,有效预防春季流感,近日,兴达路街道联合金水区志愿服务总队开展“大手拉小手”助学...
-
2023-03-28 15:38:29
3月28日山东神驰化工集团油品报价上涨
产品3月27日3月28日涨跌单位:元 吨-10 柴油71407290150元 吨92 汽油81008290190元 吨95 汽油81808370190元 吨3月
-
2023-03-28 13:56:55
【环球新要闻】这个“网络号贩子”团伙抓了!用软件抢号一万余个获利200多万
这个“网络号贩子”团伙抓了!用软件抢号一万余个获利200多万
-
2023-03-28 12:47:07
世界微动态丨2023年3月28日早盘期货要闻汇总1.在27日举行的第三届陆家嘴全球资产管理高峰论坛上,《浦东新区加快推进陆家嘴金融城全球资产管理中心核心功能区建设的若干意见》发布,其中提出,支持QFII、RQFII投资全国中小企业股份转让系统挂牌证券、私募投资基金、金融期货和商品期货、期权等
2023年3月28日早盘期货要闻汇总1 在27日举行的第三届陆家嘴全球资产管理高峰论坛上,《浦东新区加快推进陆家嘴金融城全球资产管理中心核心功能
-
2023-03-28 10:59:15
全球关注:2023年3月28日溶剂油价格最新行情预测
据中国报告大厅对2023年3月28日溶剂油价格最新走势监测显示:2023年3月28日溶剂油均价8500 0元 吨,上一报
-
2023-03-28 10:00:12
大厂降负 PTA低位反弹
大厂降负PTA低位反弹,原油,聚酯,降负,pta,开机率,开工率
-
2023-03-28 09:09:38
我汇吃食堂|每天推出两款特价菜,还有美味炸串可供加餐_环球今日讯
现在社区食堂越来越多那么最近的社区食堂在哪里?每家社区食堂有什么特点?每家社区食堂有什么好吃的?关注《我汇吃食堂》系列《
-
2023-03-28 08:00:05
社会教育的具体内容_社会教育的重要性
1、社会教育的重要性1、社会教育有广义狭义之分,广义的社会教育指与学校教育、家庭教育并行的影响个人身心发展的社会教育活动
-
2023-03-28 05:14:36
天天热议:荆楚理工教务管理系统_荆楚理工教务系统入口
1、荆楚理工学院校园网教务系统网址是荆楚理工学院校园网教务系统。2、荆楚理工学院是2007年3月经教育部批准成立的省属普
-
2023-03-28 00:47:18
韩媒:美军“尼米兹”号核航母28日到访釜山
参考消息网3月27日报道据韩联社报道,韩国国防部3月27日表示,美国“尼米兹”号核动力航空母舰领衔的“第11航母打击群”将于28日驶入釜山作...
-
2023-03-27 22:09:53
华天科技:2022年归母净利润下降46.7%,降幅超营收
华天科技于2023年3月28日披露年报,公司2022年实现营业总收入119 06亿元,同比下降1 6%;实现归母净利润7 54亿元,同比下降46 7%;每股收益为0 24元。
-
2023-03-27 20:15:14
【世界独家】(furry文)异世之旅 第二十章 大迁徙
双更,没想到吧哈哈寡淡的阅读量是唯一支持我更下去的动力小T每天都起的很早,然后在客厅等着龙可“嘿,龙可,你醒了。”“嗯哼,今天该去...
-
2023-03-27 18:47:36
东莞哪里好玩的地方_东莞哪里好玩-环球观察
1、1 隐贤山庄,推荐指数:推荐理由:老少皆宜的好去处。门票价格:30元,开放时间:7:30-19:30。位于东莞常平镇
-
2023-03-27 17:34:48
CSS in JS (JSS)|环球观速讯
简单来说,一句话概括CSSinJS(JSS),就是 行内样式 (inlinestyle)和 行内脚本 (inlinescript)。
-
2023-03-27 16:10:19
【天天速看料】26个字母按相同音素归类_按音素给26个字母分类
1、26个英文字母及发音音标如下:Aa[ei]Bb[bi:]Cc[si:]Dd[di:]Ee
-
2023-03-27 15:17:42
马云来云谷学校讨论未来教育:要用人工智能去解决问题而不是被控制_环球新消息
马云说,ChatGPT这一类技术已经对教育带来挑战,但是ChatGPT这一类技术只是AI时代的开始。我们要用人工智能(161631)去解决问题,而不是被人工智能
-
2023-03-27 13:44:53
记者观察:京津冀一体化要做大民营经济 突破物理空间对人才束缚|全球热推荐
中国的民营企业从2012年的1000多万家增长到2022年的4700多万家,在10年当中增长了两倍多。在这当中长三角地区民营经济的占比非常之大,在中国
-
2023-03-27 12:03:09
中金:维持周生生中性评级 目标价10港元 环球关注
中金发布研究报告称,维持周生生(00116)“中性”评级,考虑到黄金产品占比提升带来毛利率结构性下降,下调2023年EPS预测18%至1 37港元,引入2024
-
2023-03-27 10:49:26
左脑右脑的功能分别是_左脑偏头痛是为什么 天天微动态
1、如果患者左脑有偏头痛,比如左脑有脉冲样的抽痛,这种情况需要考虑偏头痛的可能。2、偏头痛一般是局部血管扩张或痉挛引起的
-
2023-03-27 09:46:28
焦点播报:五一假期出境游进入预订高峰 欧洲游热度走高
随着第二批出境团队旅游业务的试点恢复,目前国内出境团队游目的地已增加到60个。记者近日了解到,目前“五一”假期出境游产品
-
2023-03-27 08:36:19
环球动态:深圳华强(000062)3月24日主力资金净买入3583.57万元
截至2023年3月24日收盘,深圳华强(000062)报收于14 3元,上涨6 4%,换手率2 09%,成交量21 81万手,成交额3 05亿元。
-
2023-03-27 06:16:59
教坛新秀展风采 不负韶华向未来|最新资讯
许昌实验中学教坛新秀展风采不负韶华向未来本报讯(记者吕正子通讯员王利兵)为进一步加强青年教师队伍建设,打造一支教学基本功扎实、专业素
-
2023-03-27 01:09:27
当前焦点!more than one +单数名词_more than one单复数
今天小编肥嘟来为大家解答以上的问题。morethanone+单数名词,morethanone单复数相信很多小伙伴还不知道,现在让我们一起来看看吧!1、
-
2023-03-26 22:02:37
钱在键盘上怎么打出来_钱的符号在键盘上怎么打_环球消息
1、+4(不是小键盘的)就可以打出¥或者$。2、编程里$对应的ASCII码是十进制的36。本文到此分享完毕,希望对你有
-
2023-03-26 19:10:42
环球快看点丨索尼cr392主板型号_索尼cr392
1、可以换cpu。2、∴。3、!。以上就是【索尼cr392主板型号,索尼cr392】相关内容。
-
2023-03-26 17:06:32
全球今头条!2022年法考主观题开考,多名江苏考生认为题目难
2022年法考主观题开考,多名江苏考生认为题目难
-
2023-03-26 14:57:57
做自然的朋友,罗湖企业连续15年为地球献出一小时
3月25日,京基集团及旗下产业积极响应由WWF(世界自然基金会)发起的全球最大的应对气候变化的公益行动——“地球一小时”。在当天晚上8点3...