Golang的五种原子操作用法详解原子操作顾名思义就是原子操作……是不是感觉说了和不说一样?原子性的解释如下:CPU执行过程中一个或多个操作不被中断的特性称为原子性(atomicity)。这些操作作为一个不可分割的整体呈现给外界。它们要么被执行,要么不被执行。外界不会看到他们只被处决了一半。CPU不可能不间断地执行一系列操作,但如果我们能够在执行多个操作时让它们的中间状态对外界不可见,那么我们就可以宣称它们具有“不可分割”的原子性。我们在数据库事务的ACID概念中听到过类似的解释。Go语言提供了哪些原子操作?Go语言内置的包sync/atomic提供了对原子操作的支持。它提供的原子操作包括以下几类:它已被更改。操作方法的命名方式为LoadXXXType。支持的类型除了基本类型外,还支持Pointer,即支持加载任意类型的指针。存储,有加载就一定有存储操作。该类操作的方法名以Store开头,支持的类型与加载操作支持的类型相同。CASGoCAS交换,这个简单粗暴,不是直接交换,这个操作很少用到。互斥锁和原子操作的区别日常在并发编程中,为了保证并发安全,经常会用到Go语言的sync包中的同步原语Mutex,那么它和这些操作有什么区别呢?原子包?在我看来,它们在使用目的和底层实现上是不同的:使用目的:mutex用于保护一段逻辑,原子操作用于更新和保护一个变量。底层实现:Mutex由操作系统的调度器实现,原子包中的原子操作由底层硬件指令直接支持。这些指令在执行过程中不允许被打断,因此可以在无锁的情况下进行原子操作,并发安全在一定情况下得到保证,其性能也可以随着CPU数量的增加而线性扩展。为了保护变量更新,原子操作通常效率更高,可以更好地利用计算机的多核优势。例如,下面是一个使用互斥量的并发计数器程序:funcmutexAdd(){varaint32=0varwgsync.WaitGroupvarmusync.Mutexstart:=time.Now()fori:=0;i<100000000;i++{wg.Add(1)gofunc(){deferwg.Done()mu.Lock()a+=1mu.Unlock()}()}wg.Wait()timeSpends:=time.Now().Sub(start).Nanoseconds()fmt.Printf("usemutexais%d,spendtime:%v\n",a,timeSpends)}改变方法atomic.AddInt32(&a,1)调用的Mutex,仍然可以在不加锁的情况下保证变量自增的并发安全。funcAtomicAdd(){varaint32=0varwgsync.WaitGroupstart:=time.Now()fori:=0;i<1000000;i++{wg.Add(1)gofunc(){deferwg.Done()atomic.AddInt32(&a,1)}()}wg.Wait()timeSpends:=time.Now().Sub(start).Nanoseconds()fmt.Printf("useatomicais%d,spendtime:%v\n",atomic.LoadInt32(&a),timeSpends)}可以在本地运行上面两段代码,可以观察到最后counter的结果都是1000000,是线程安全的。需要注意的是,所有原子操作方法的操作数参数必须是指针类型,可以通过指针变量获取操作数在内存中的地址,从而应用专门的CPU指令来保证在同一时刻只能运行一个goroutine同时。除了加法操作,上面的例子还演示了加载操作。接下来,让我们看一下CAS操作。这种比较和交换的操作简称为CAS(CompareAndSwap)。这类操作的前缀是CompareAndSwap:funcCompareAndSwapInt32(addr*int32,old,newint32)(swappedbool)funcCompareAndSwapPointer(addr*unsafe.Pointer,old,newunsafe.Pointer)(swappedbool)这个操作首先保证操作数之前的值swapping的值没有被改变,即参数old记录的值仍然被保存,只有满足这个前置条件才会进行exchange操作。CAS的做法类似于操作数据库时常用的乐观锁机制。需要注意的是,当大量的goroutines读写变量时,可能会导致CAS操作失败。这时候可以使用for循环多次尝试。上面我只列举了典型的int32和unsafe.Pointer类型的CAS方法。主要想说除了读取值类型的比较交换之外,还支持指针的比较交换。unsafe.Pointer提供了一种绕过Go语言指针类型限制的方法,unsafe不代表不安全,只是官方不保证向后兼容。//定义一个struct类型PtypePstruct{x,y,zint}//执行P类型的指针varpP*Pfuncmain(){//定义一个执行unsafe.Pointer值的指针变量varunsafe1=(*unsafe.Pointer)(unsafe.Pointer(&pP))//OldpointervarsyP//为了演示效果,先将unsafe1设置为OldPointerpx:=atomic.SwapPointer(unsafe1,unsafe.Pointer(&sy))//执行CAS操作,交换成功,结果返回truey:=atomic。CompareAndSwapPointer(unsafe1,unsafe.Pointer(&sy),px)fmt.Println(y)}上面的例子并不是并发环境下的CAS,只是为了演示效果,先将操作数设置为OldPointer。事实上,Mutex的底层实现也依赖于原子操作中的CAS。原子操作的原子包相当于sync包中同步原语的实现依赖。比如互斥锁Mutex的结构体中有一个state域,它是一个状态位,表示锁的状态。typeMutexstruct{stateint32semauint32}为了便于理解,我们在这里将它的状态定义为0和1,0表示锁当前空闲,1表示已经上锁。以下是sync.Mutex中Lock方法的部分实现代码。func(m*Mutex)Lock(){//Fastpath:grabunlockedmutex.ifatomic.CompareAndSwapInt32(&m.state,0,mutexLocked){ifrace.Enabled{race.Acquire(unsafe.Pointer(m))}return}//慢速路径(outlinedssothatthefastpathcanbeinlined)m.lockSlow()}在atomic.CompareAndSwapInt32(&m.state,0,mutexLocked)中,m.state表示锁的状态。通过CAS方法,判断此时锁的状态是否空闲(m.state==0),如果是则加锁(mutexLocked常量值为1)。atomic.Value保证任意值的读写安全。atomic包提供了一组以Store开头的方法,保证各类变量的并发写安全,防止其他操作在修改变量的过程中读到脏数据。funcStoreInt32(addr*int32,valint32)funcStoreInt64(addr*int64,valint64)funcStorePointer(addr*unsafe.Pointer,valunsafe.Pointer)...这些操作方法的定义和上面介绍的类似,我就不演示了使用这些方法。值得一提的是,如果想并发且安全地设置一个结构体的多个字段,除了将结构体转化为指针并通过StorePointer进行设置外,还可以使用后面的atomic包引入的atomic.Value,就是我们完成了从具体指针类型到unsafe.Pointer的转换。有了atomic.Value,就可以让我们不依赖不保证兼容性的unsafe.Pointer类型,同时将任意数据类型的读写操作封装成原子操作(中间状态对外界不可见)).atomic.Value类型暴露了两个方法:v.Store(c)catomic.Valuevc:=v.Load()-读操作,从线程安全的v中读取上一步存储的内容。我觉得1.17版本还添加了Swap和CompareAndSwap方法。简洁的界面使其易于使用,只需将需要并发保护的变量读取和赋值操作替换为Load()和Store()。由于Load()返回的是interface{}类型,所以我们记得在使用之前将其转换为特定类型的值。下面是一个演示atomic.Value用法的简单示例。typeRectanglesstruct{lengthintwidthint}varrectatomic.Valuefuncupdate(width,lengthint){rectLocal:=new(Rectangle)rectLocal.width=widththrectLocal.length=lenghrect.Store(rectLocal)}funcmain(){wg:=sync.WaitGroup{}wg.Add(10)//10个协程并发更新fori:=0;i<10;i++{gofunc(){deferwg.Done()update(i,i+5)}()}wg.Wait()_r:=rect.Load().(*Rectangle)fmt.Printf("rect.width=%d\nrect.length=%d\n",_r.width,_r.length)}你也可以试试不用atomic.Value,直接给Rectange类型的指针变量赋值,看并发情况下两个字段的值能否如预期的变成10和15。小结本文详细介绍了Go语言原子操作atomic包中常用操作的使用场景和用法。当然,我并没有列出atomic包中所有操作的用法,主要是因为其中的一些操作是在不同的地方使用的。很多,或者已经换成更好的方法了,实在是没必要。看完本文的内容,相信你完全有能力自己去探索原子包。同样,底层硬件支持原子操作,而锁由操作系统的调度程序实现。应该使用锁来保护一段逻辑。为了保护变量更新,原子操作通常更高效并且可以利用多核计算机。如果要更新一个复合对象,应该用atomic.Value完成封装。尽快给网管打个star吸取我的知识吧:point_up_2:
