原文: http://ifeve.com/go-concurrency-atomic/golang
1. 什么是原子操做安全
咱们已经知道,原子操做便是进行过程当中不能被中断的操做。也就是说,针对某个值的原子操做在被进行的过程中,CPU毫不会再去进行其它的针对该值的操做。不管这些其它的操做是否为原子操做都会是这样。为了实现这样的严谨性,原子操做仅会由一个独立的CPU指令表明和完成。只有这样才可以在并发环境下保证原子操做的绝对安全。
Go语言提供的原子操做都是非侵入式的。它们由标准库代码包sync/atomic中的众多函数表明。咱们能够经过调用这些函数对几种简单的类型的值进行原子操做。架构
2.goalng 中的原子操做类型并发
int3二、int6四、uint3二、uint6四、uintptr和unsafe.Pointer类型,共6个app
3.golang 中有哪些原子操做函数
有5种,即:增或减、比较并交换、载入、存储和交换。性能
4.详解ui
1. 增或减
被用于进行增或减的原子操做(如下简称原子增/减操做)的函数名称都以“Add”为前缀,并后跟针对的具体类型的名称。例如,实现针对uint32类型的原子增/减操做的函数的名称为AddUint32。事实上,sync/atomic包中的全部函数的命名都遵循此规则。atom
2. 比较并交换
有些读者可能很熟悉比较并交换操做的英文称谓——Compare And Swap,简称CAS。在sync/atomic包中,这类原子操做由名称以“CompareAndSwap”为前缀的若干个函数表明。
咱们依然以针对int32类型值的函数为例。该函数名为CompareAndSwapInt32。其声明以下:指针
1 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) |
能够看到,CompareAndSwapInt32函数接受三个参数。第一个参数的值应该是指向被操做值的指针值。该值的类型即为*int32。后两个参数的类型都是int32类型。它们的值应该分别表明被操做值的旧值和新值。CompareAndSwapInt32函数在被调用以后会先判断参数addr指向的被操做值与参数old的值是否相等。仅当此判断获得确定的结果以后,该函数才会用参数new表明的新值替换掉原先的旧值。不然,后面的替换操做就会被忽略。这正是“比较并交换”这个短语的由来。CompareAndSwapInt32函数的结果swapped被用来表示是否进行了值的替换操做。
与咱们前面讲到的锁相比,CAS操做有明显的不一样。它老是假设被操做值不曾被改变(即与旧值相等),并一旦确认这个假设的真实性就当即进行值替换。而使用锁则是更加谨慎的作法。咱们老是先假设会有并发的操做要修改被操做值,并使用锁将相关操做放入临界区中加以保护。咱们能够说,使用锁的作法趋于悲观,而CAS操做的作法则更加乐观。
CAS操做的优点是,能够在不造成临界区和建立互斥量的状况下完成并发安全的值替换操做。这能够大大的减小同步对程序性能的损耗。固然,CAS操做也有劣势。在被操做值被频繁变动的状况下,CAS操做并不那么容易成功。有些时候,咱们可能不得不利用for循环以进行屡次尝试。示例以下:
var value int32 func addValue(delta int32) { for { v := value if atomic.CompareAndSwapInt32(&value, v, (v + delta)) { break } } }
能够看到,为了保证CAS操做的成功完成,咱们仅在CompareAndSwapInt32函数的结果值为true时才会退出循环。这种作法与自旋锁的自旋行为类似。addValue函数会不断的尝试原子的更新value的值,直到这一操做成功为止。操做失败的原因总会是value的旧值已不与v的值相等了。若是value的值会被并发的修改的话,那么发生这种状况是很正常的。
CAS操做虽然不会让某个Goroutine阻塞在某条语句上,可是仍可能会使流程的执行暂时停滞。不过,这种停滞的时间大都极其短暂。
请记住,当想并发安全的更新一些类型(更具体的讲是,前文所述的那6个类型)的值的时候,咱们老是应该优先选择CAS操做。
与此对应,被用来进行原子的CAS操做的函数共有6个。除了咱们已经讲过的CompareAndSwapInt32函数以外,还有CompareAndSwapInt6四、CompareAndSwapPointer、CompareAndSwapUint3二、CompareAndSwapUint64 和CompareAndSwapUintptr函数。这些函数的结果声明列表与CompareAndSwapInt32函数的彻底一致。而它们的参数声明列表与后者也很是相似。虽然其中的那三个参数的类型不一样,但其遵循的规则是一致的,即:第二个和第三个参数的类型均为与第一个参数的类型(即某个指针类型)紧密相关的那个类型。例如,若是第一个参数的类型为*unsafe.Pointer,那么后两个参数的类型就必定是unsafe.Pointer。这也是由这三个参数的含义决定的。
3. 载入
在前面示例的for循环中,咱们使用语句v := value为变量v赋值。可是,要注意,其中的读取value的值的操做并非并发安全的。在该读取操做被进行的过程当中,其它的对此值的读写操做是能够被同时进行的。它们并不会受到任何限制。
在第7章的第1节的最后,咱们举过这样一个例子:在32位计算架构的计算机上写入一个64位的整数。若是在这个写操做未完成的时候有一个读操做被并发的进行了,那么这个读操做极可能会读取到一个只被修改了一半的数据。这种结果是至关糟糕的。
为了原子的读取某个值,sync/atomic代码包一样为咱们提供了一系列的函数。这些函数的名称都以“Load”为前缀,意为载入。咱们依然以针对int32类型值的那个函数为例。
咱们下面利用LoadInt32函数对上一个示例稍做修改:制。
func addValue(delta int32) { for { v := atomic.LoadInt32(&value) if atomic.CompareAndSwapInt32(&value, v, (v + delta)) { break } } }
函数atomic.LoadInt32接受一个*int32类型的指针值,并会返回该指针值指向的那个值。在该示例中,咱们使用调用表达式atomic.LoadInt32(&value)替换掉了标识符value。替换后,那条赋值语句的含义就变为:原子的读取变量value的值并把它赋给变量v。有了“原子的”这个形容词就意味着,在这里读取value的值的同时,当前计算机中的任何CPU都不会进行其它的针对此值的读或写操做。这样的约束是受到底层硬件的支持的。
注意,虽然咱们在这里使用atomic.LoadInt32函数原子的载入value的值,可是其后面的CAS操做仍然是有必要的。由于,那条赋值语句和if语句并不会被原子的执行。在它们被执行期间,CPU仍然可能进行其它的针对value的值的读或写操做。也就是说,value的值仍然有可能被并发的改变。
与atomic.LoadInt32函数的功能相似的函数有atomic.LoadInt6四、atomic.LoadPointer、atomic.LoadUint3二、atomic.LoadUint64和atomic.LoadUintptr。
4. 存储
与读取操做相对应的是写入操做。而sync/atomic包也提供了与原子的值载入函数相对应的原子的值存储函数。这些函数的名称均以“Store”为前缀。
5. 交换在sync/atomic代码包中还存在着一类函数。它们的功能与前文所讲的CAS操做和原子载入操做都有些相似。这样的功能能够被称为原子交换操做。这类函数的名称都以“Swap”为前缀。