略微加速

略速 - 互联网笔记

golang 原子操作

2021-02-24 leiting (1695阅读)

标签 Golang

1. 什么是原子操作

  我们已经知道,原子操作即是进行过程中不能被中断的操作。也就是说,针对某个值的原子操作在被进行的过程当中,CPU绝不会再去进行其它的针对该值的操作。无论这些其它的操作是否为原子操作都会是这样。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。只有这样才能够在并发环境下保证原子操作的绝对安全。
Go语言提供的原子操作都是非侵入式的。它们由标准库代码包sync/atomic中的众多函数代表。我们可以通过调用这些函数对几种简单的类型的值进行原子操作。

2.goalng 中的原子操作类型

  int32、int64、uint32、uint64、uintptr和unsafe.Pointer类型,共6个

3.golang 中有哪些原子操作

  有5种,即:增或减、比较并交换、载入、存储和交换。

4.详解

   1. 增或减
被用于进行增或减的原子操作(以下简称原子增/减操作)的函数名称都以“Add”为前缀,并后跟针对的具体类型的名称。例如,实现针对uint32类型的原子增/减操作的函数的名称为AddUint32。事实上,sync/atomic包中的所有函数的命名都遵循此规则。

  2. 比较并交换
有些读者可能很熟悉比较并交换操作的英文称谓——Compare And Swap,简称CAS。在sync/atomic包中,这类原子操作由名称以“CompareAndSwap”为前缀的若干个函数代表。
我们依然以针对int32类型值的函数为例。该函数名为CompareAndSwapInt32。其声明如下:

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函数之外,还有CompareAndSwapInt64、CompareAndSwapPointer、CompareAndSwapUint32、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.LoadInt64、atomic.LoadPointer、atomic.LoadUint32、atomic.LoadUint64和atomic.LoadUintptr。

  4. 存储
与读取操作相对应的是写入操作。而sync/atomic包也提供了与原子的值载入函数相对应的原子的值存储函数。这些函数的名称均以“Store”为前缀。

 

  5. 交换
在sync/atomic代码包中还存在着一类函数。它们的功能与前文所讲的CAS操作和原子载入操作都有些类似。这样的功能可以被称为原子交换操作。这类函数的名称都以“Swap”为前缀。


原文: http://ifeve.com/go-concurrency-atomic/

北京半月雨文化科技有限公司.版权所有 京ICP备12026184号-3