1. 什么是CAS原子操作

  1. 原子操作即是进行过程中不能被中断的操作。也就是说,针对某个值的原子操作在被进行的过程当中,CPU绝不会再去进行其它的针对该值的操作。无论这些其它的操作是否为原子操作都会是这样。
  2. 为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。只有这样才能够在并发环境下保证原子操作的绝对安全。
  3. CAS操作的优势是,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。 当想并发安全的更新一些类型的值的时候,我们总是应该优先选择CAS操作。
  4. CAS操作也有劣势,在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。
  5. CAS操作虽然不会像Mutex让某个Goroutine阻塞在某条语句上,但是仍可能会使流程的执行暂时停滞。不过,这种停滞的时间大都极其短暂。

2. 用原子操作来替换mutex锁

原子操作由底层硬件支持,而锁则由操作系统提供的API实现。若实现相同的功能,前者通常会更有效率。

  1. df.rmutex.Lock()
  2. defer df.rmutex.Unlock()
  3. return df.roffset / int64(df.dataLen)
  4. // 现在去掉施加在上面的锁定和解锁操作,转而使用原子操作来实现它。修改后的代码如下:
  5. offset := atomic.LoadInt64(&df.roffset)
  6. return offset / int64(df.dataLen)

3. go 的原子操作

Go语言提供的原子操作都是非侵入式的。它们由标准库代码包 sync/atomic 中的众多函数代表。我们可以通过调用这些函数对几种简单的类型的值进行原子操作。这些类型包括 int32int64uint32uint64uintptrunsafe.Pointer 类型,共6个。这些函数提供的原子操作共有5种,即: 增或减比较并交换载入存储交换 。它们分别提供了不同的功能,且适用的场景也有所区别。下面,我们就根据这些种类对Go语言提供的原子操作进行逐一的讲解。

3.1. 增或减

  1. 被用于进行增或减的原子操作的函数名称都以“Add”为前缀,并后跟针对的具体类型的名称。

    注意,并不存在名为atomic.AddPointer的函数,因为unsafe.Pointer类型值之间既不能被相加也不能被相减。

  1. var i32 int32 = 3
  2. atomic.AddInt32(&i32,int32(1))
  3. fmt.Println(i32) // 4


不过,由于atomic.AddUint32函数和atomic.AddUint64函数的第二个参数的类型分别是uint32和uint64,所以我们无法通过传递一个负的数值来减小被操作值,我们可以通过官方推荐的二进制补码的特性来实现

  1. var ui32 uint32 = 2
  2. var ui64 uint64 = 4
  3. N := -2 // N代表了一个负整数
  4. atomic.AddUint32(&ui32, ^uint32(-N-1))
  5. atomic.AddUint64(&ui64, ^uint64(-N-1))
  6. fmt.Println(ui32,ui64) // 0 2

3.2. 比较并交换

  1. 被用于进行比较并交换原子操作的函数名称都以“CompareAndSwapI”为前缀,并后跟针对的具体类型的名称。
  2. func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
    CompareAndSwapInt32函数在被调用之后会先判断参数addr指向的被操作值与参数old的值是否相等。仅当此判断得到肯定的结果之后,该函数才会用参数new代表的新值替换掉原先的旧值。否则,后面的替换操作就会被忽略。返回值用来表示是否进行了值的替换操作
  3. 与我们前面讲到的 Mutex 锁相比,CAS操作有明显的不同。它总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。而使用锁则是更加谨慎的做法。我们总是先假设会有并发的操作要修改被操作值,并使用锁将相关操作放入临界区中加以保护。我们可以说,使用锁的做法趋于悲观,而CAS操作的做法则更加乐观
  4. CAS操作的优势是,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。当然,CAS操作也有劣势。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。有些时候,我们可能不得不利用for循环以进行多次尝试。示例如下:
  1. var value int32
  2. func main() {
  3. addValue(3)
  4. }
  5. func addValue(delta int32) {
  6. for {
  7. v := value
  8. if atomic.CompareAndSwapInt32(&value, v, (v + delta)) {
  9. break
  10. }
  11. }
  12. }

可以看到,为了保证CAS操作的成功完成,我们仅在CompareAndSwapInt32函数的结果值为true时才会退出循环。这种做法与自旋锁的自旋行为相似。addValue函数会不断的尝试原子的更新value的值,直到这一操作成功为止。操作失败的缘由总会是value的旧值已不与v的值相等了。如果value的值会被并发的修改的话,那么发生这种情况是很正常的。

3.3. 载入

  1. 举个例子:在32位计算架构的计算机上写入一个64位的整数。如果在这个写操作未完成的时候有一个读操作被并发的进行了,那么这个读操作很可能会读取到一个只被修改了一半的数据。这种结果是相当糟糕的。
  2. 为了原子的读取某个值sync/atomic代码包同样为我们提供了一系列的函数。这些函数都以”Load”为前缀,意为载入。在这里读取value的值的同时,当前计算机中的任何CPU都不会进行其它的针对此值的读或写操作。这样的约束是受到底层硬件的支持的。
  1. fun addValue(delta int32){
  2. for{
  3. v := atomic.LoadInt32(&value)
  4. if atomic.CompareAndSwapInt32(&value,v,(delta+v)){
  5. break
  6. }
  7. }
  8. }

注意,虽然我们在这里使用atomic.LoadInt32函数原子的载入value的值,但是其后面的CAS操作仍然是有必要的。因为,那条赋值语句和if语句并不会被原子的执行。在它们被执行期间,CPU仍然可能进行其它的针对value的值的读或写操作。也就是说,value的值仍然有可能被并发的改变。

3.4. 存储

  1. 与读操作对应的是写入操作,sync/atomic也提供了与原子的值载入函数相对应的原子的值存储函数。这些函数的名称均以“Store”为前缀。
  2. 在原子的存储某个值的过程中,任何cpu都不会进行针对进行同一个值的读或写操作。如果我们把所有针对此值的写操作都改为原子操作,那么就不会出现针对此值的读操作读操作因被并发的进行而读到修改了一半的情况
  3. 原子的值存储操作总会成功,因为他不必关心被操作值的旧值是什么。
  1. func StoreInt32(addr *int32 /* 被操作值的指针 */, val int32 /* 新值 */)
  2. atomic.StoreInt32(&value,newaddr)

3.5. 交换

  1. 原子交换操作,这类函数的名称都以“Swap”为前缀。
  2. 与CAS不同,交换操作直接设置新值,不会关心被操作值的旧值,但它又比原子载入操作多做了一步。作为交换,它会返回被操作值的旧值。此类操作比CAS操作的约束更少,同时又比原子载入操作的功能更强。
  1. func SwapInt32(addr *int32 /* 被操作值的指针 */, new int32 /* 新值 */) (old int32 /* 返回旧值 */)
  2. oldval:= atomic.StoreInt32(&value,newaddr)

4. 感谢

http://ifeve.com/go-concurrency-atomic/
https://studygolang.com/articles/3557