Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如goroutine和channel同步原语,库层面有sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和工具类(Once、WaitGroup、Cond、Pool、Map)syncatomic:提供变量的原子操作(基于硬件指令compareandswap) atomic 当我们想要对某个变量并发安全的修改,除了使用官方提供的mutex,还可以使用syncatomic包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。 atomic包的原子操作是通过CPU指令,也就是在硬件层次去实现的,性能较好,不需要像mutex那样记录很多状态。当然,mutex不止是对变量的并发控制,更多的是对代码块的并发控制,2者侧重点不一样。 atomic这些功能需要非常小心才能正确使用。除了特殊的低级应用程序外,最好使用通道或同步包的工具来完成同步。通过通信共享内存;不要通过共享内存进行通信。 atomic包有几种原子操作,主要是Add、CompareAndSwap、Load、Store、SwapAdd atomic的Add是针对int和uint进行原子加值的: 当需要添加的值为负数的时候,做减法,正数做加法funcAddInt32(addrint32,deltaint32)(newint32)funcAddUint32(addruint32,deltauint32)(newuint32)funcAddInt64(addrint64,deltaint64)(newint64)funcAddUint64(addruint64,deltauint64)(newuint64)funcAddUintptr(addruintptr,deltauintptr)(newuintptr)CompareAndSwap 比较并交换方法实现了类似乐观锁的功能,只有原来的值和传入的old值一样,才会去修改, CAS操作,会先比较传入的地址的值是否是old,如果是的话就尝试赋新值,如果不是的话就直接返回false,返回true时表示赋值成功。 funcCompareAndSwapInt32(addrint32,old,newint32)(swappedbool)funcCompareAndSwapInt64(addrint64,old,newint64)(swappedbool)funcCompareAndSwapUint32(addruint32,old,newuint32)(swappedbool)funcCompareAndSwapUint64(addruint64,old,newuint64)(swappedbool)funcCompareAndSwapUintptr(addruintptr,old,newuintptr)(swappedbool)funcCompareAndSwapPointer(addrunsafe。Pointer,old,newunsafe。Pointer)(swappedbool) 需要注意的是,CompareAndSwap有可能产生ABA现象发生。也就是原来的值是A,后面被修改B,再后面修改为A。在这种情况下也符合了CompareAndSwap规则,即使中途有被改动过。Load 从某个地址中取值 Load方法是为了防止在读取过程中,有其他协程发起修改动作,影响了读取结果,常用于配置项的整个读取。funcLoadInt32(addrint32)(valint32)funcLoadInt64(addrint64)(valint64)funcLoadUint32(addruint32)(valuint32)funcLoadUint64(addruint64)(valuint64)funcLoadUintptr(addruintptr)(valuintptr)funcLoadPointer(addrunsafe。Pointer)(valunsafe。Pointer)Store 给某个地址赋值 有原子读取,就有原子修改值,前面提到过的Add只适用于int、uint类型的增减,并没有其他类型的修改,而Sotre方法通过unsafe。Pointer指针原子修改,来达到了对其他类型的修改。 funcStoreInt32(addrint32,valint32)funcStoreInt64(addrint64,valint64)funcStoreUint32(addruint32,valuint32)funcStoreUint64(addruint64,valuint64)funcStoreUintptr(addruintptr,valuintptr)funcStorePointer(addrunsafe。Pointer,valunsafe。Pointer)Swap 交换两个值,并且返回老的值 Swap方法实现了对值的原子交换,不仅int,uint可以交换,指针也可以。funcSwapInt32(addrint32,newint32)(oldint32)funcSwapInt64(addrint64,newint64)(oldint64)funcSwapUint32(addruint32,newuint32)(olduint32)funcSwapUint64(addruint64,newuint64)(olduint64)funcSwapUintptr(addruintptr,newuintptr)(olduintptr)funcSwapPointer(addrunsafe。Pointer,newunsafe。Pointer)(oldunsafe。Pointer) value类型 最后一类Value用于任意类型的值的Store、Load,我们开始的案例就用到了这个,这是1。4版本之后引入的,签名的方法都只能作用于特定的类型,引入这个方法之后就可以用于任意类型了。 syncatomic标准库包也提供了一个Value类型,以它为基的指针类型Value拥有四个方法(见下,其中后两个是从Go1。17开始才支持的)。Value值用来原子读取和修改任何类型的Go值。func(Value)Load()(xinterface{})func(Value)Store(xinterface{})func(Value)Swap(newinterface{})(oldinterface{})func(Value)CompareAndSwap(old,newinterface{})(swappedbool)CAS 在syncatomic包中的源码除了Value之外其他的函数都是没有直接的源码的,需要去runtimeinternalatomic中找寻,这里为CAS函数为例,其他的都是大同小异boolCas(int32val,int32old,int32new)Atomically:if(valold){valnew;return1;}elsereturn0;TEXTruntimeinternalatomicCas(SB),NOSPLIT,017MOVQptr0(FP),BXMOVLold8(FP),AXMOVLnew12(FP),CXLOCKCMPXCHGLCX,0(BX)SETEQret16(FP)RET 在注释部分写的非常清楚,这个函数主要就是先比较一下当前传入的地址的值是否和old值相等,如果相等,就赋值新值返回true,如果不相等就返回false 我们看这个具体汇编代码就可以发现,使用了LOCK来保证操作的原子性,CMPXCHG指令其实就是CPU实现的CAS操作。示例对比Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如goroutine和channel同步原语,库层面有sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和工具类(Once、WaitGroup、Cond、Pool、Map)syncatomic:提供变量的原子操作(基于硬件指令compareandswap)packagemainimport(fmtsyncsyncatomictime)var(xint64mxsync。Mutexwgsync。WaitGroup)普通函数,并发不安全funcAdd(){xwg。Done()}互斥锁,并发安全,性能低于原子操作funcMxAdd(){mx。Lock()xmx。Unlock()wg。Done()}原子操作,并发安全,性能高于互斥锁,只针对go中的一些基本数据类型使用funcAmAdd(){atomic。AddInt64(x,1)wg。Done()}funcmain(){原子操作atomic包加锁操作涉及到内核态的上下文切换,比较耗时,代价高针对基本数据类型我们还可以使用原子操作来保证并发安全因为原子操作是go语言提供的方法,我们在用户态就可以完成,因此性能比加锁操作更好go语言的原子操作由内置的库,syncatomic完成start:time。Now()fori:0;i10000;i{wg。Add(1)goAdd()普通版Add函数不是并发安全的goMxAdd()加锁版Add函数,是并发安全的,但是加锁性能开销大goAmAdd()原子操作版Add函数,是并发安全的,性能优于加锁版}end:time。Now()wg。Wait()fmt。Println(x)fmt。Println(end。Sub(start))} Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如goroutine和channel同步原语,库层面有sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和工具类(Once、WaitGroup、Cond、Pool、Map)syncatomic:提供变量的原子操作(基于硬件指令compareandswap)packagemainimport(fmtsyncsyncatomictime)var(xint64mxsync。Mutexwgsync。WaitGroup)普通函数,并发不安全funcAdd(){xwg。Done()}互斥锁,并发安全,性能低于原子操作funcMxAdd(){mx。Lock()xmx。Unlock()wg。Done()}原子操作,并发安全,性能高于互斥锁,只针对go中的一些基本数据类型使用funcAmAdd(){atomic。AddInt64(x,1)wg。Done()}funcmain(){原子操作atomic包加锁操作涉及到内核态的上下文切换,比较耗时,代价高针对基本数据类型我们还可以使用原子操作来保证并发安全因为原子操作是go语言提供的方法,我们在用户态就可以完成,因此性能比加锁操作更好go语言的原子操作由内置的库,syncatomic完成start:time。Now()fori:0;i10000;i{wg。Add(1)goAdd()普通版Add函数不是并发安全的goMxAdd()加锁版Add函数,是并发安全的,但是加锁性能开销大goAmAdd()原子操作版Add函数,是并发安全的,性能优于加锁版}end:time。Now()wg。Wait()fmt。Println(x)fmt。Println(end。Sub(start))} Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如goroutine和channel同步原语,库层面有sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和工具类(Once、WaitGroup、Cond、Pool、Map)syncatomic:提供变量的原子操作(基于硬件指令compareandswap)packagemainimport(fmtsyncsyncatomictime)var(xint64mxsync。Mutexwgsync。WaitGroup)普通函数,并发不安全funcAdd(){xwg。Done()}互斥锁,并发安全,性能低于原子操作funcMxAdd(){mx。Lock()xmx。Unlock()wg。Done()}原子操作,并发安全,性能高于互斥锁,只针对go中的一些基本数据类型使用funcAmAdd(){atomic。AddInt64(x,1)wg。Done()}funcmain(){原子操作atomic包加锁操作涉及到内核态的上下文切换,比较耗时,代价高针对基本数据类型我们还可以使用原子操作来保证并发安全因为原子操作是go语言提供的方法,我们在用户态就可以完成,因此性能比加锁操作更好go语言的原子操作由内置的库,syncatomic完成start:time。Now()fori:0;i10000;i{wg。Add(1)goAdd()普通版Add函数不是并发安全的goMxAdd()加锁版Add函数,是并发安全的,但是加锁性能开销大goAmAdd()原子操作版Add函数,是并发安全的,性能优于加锁版}end:time。Now()wg。Wait()fmt。Println(x)fmt。Println(end。Sub(start))} 原子操作是比其它同步技术更基础的操作。原子操作是无锁的,常常直接通过CPU指令直接实现。事实上,其它同步技术的实现常常依赖于原子操作。例如上面的的mx。lock atomic包提供了底层的内存操作,对于同步算法的实现很有用,这些函数必须谨慎的保证正确使用,除了某些特殊的底层应用,使用通道或者sync包的函数类型实现同步更好 atomic很多时候可能都没有使用上,毕竟mutex的拓展性比较好,使用起来也比较友好。但这并不妨碍我们对极致性能的追求,有时候,细节决定了性能!ReferencesGolang:sync。OnceSynchronization(ComputerScience))SIP20ImprovedLazyValsInitializationsyncatomicGoPackagesGolangpackagesync剖析(一):sync。Oncesyncatomic标准库包中提供的原子操作Go并发编程(五)深入理解syncatomicGo语言标准库中atomic。Value的前世今生深入浅出Gosyncatomic源码分析