C++并发编程 | CAS基本原理

关于volatile

  • volatile告诉编译器,当编译器遇到这个变量的时候,只能从变量的内存地址中读取这个变量,不可以从缓存、寄存器、或者其它任何地方读取。
  • 两个包含volatile变量的指令,编译后不可以乱序

C/C++中的volatile并不是用来解决多线竞争问题的,而是用来修饰一些程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,来提醒编译器不要对该变量的访问擅自进行优化。

C++11标准明确指出解决多线程的数据竞争问题应该使用原子操作互斥锁

CAS的基本原理

高并发服务器经常用到多线程编程,需要对共享数据进行操作,为了保证数据的正确性,有一种有效的方法就是加锁机制,但是这种方式存在以下一些缺点:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 一个线程锁持有锁会导致其它所有所有需要此锁的线程挂起
  • 如果一个优先级较高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险;

为了解决多线程并行情况下使用锁造成性能损耗的问题,我们引入了CAS机制(Compare and Swap)

CAS结构

CAS包含三个操作数:

  • 内存位置 (V) ------ 是个指针
  • 预期原值 (A)
  • 新值(B)

如果内存位置V内保存的值预期原值匹配,那么处理器会总动将内存位置V处的值更新为新值否则处理器不做任何操作

无论哪种情况,它都会返回内存位置原来保存的值。

示例如下,判断内存reg里的值是不是oldval,如果是的话,则对其赋值newval。

int compare_and_swap (int* reg, int oldval, int newval)
{
  int old_reg_val = *reg;
  if(old_reg_val == oldval)
     *reg = newval;
     
  return old_reg_val;
}

这个操作可以变种为返回bool值的形式(返回bool值的好处在于,调用者可以知道有没有更新成功) :

bool compare_and_swap (int* reg, int oldval, int newval)
{
  if(*reg == oldval)
  {
  	 *reg = newval;
  	 return true;
  }
  return false;
}

CAS是一种有名的无锁算法。无锁编程即不适用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步。

CAS总结如下:

  • CAS(Compare and Swap)比较并替换,是线程并发运行时用到的一种技术
  • CAS是原子操作,保证并发安全,而不能保证并发同步
  • CAS是CPU的一个指令
  • CAS是非阻塞的轻量级的乐观锁

ABA问题

所谓ABA问题如下:

  • 线程P1在共享变量中读到值为A;
  • 线程P1被抢占了,线程P2执行,P2把共享变量的值从A改成了B;
  • 线程P3将共享变量又从B改回到A,此时被P1抢占;
  • P1回来看到共享变量里面的值没有被改变,于是继续执行;

现实场景:

  • 小明银行卡里有100元,此时小明需要取50元,由于系统阻塞,取操作执行两次;
  • 在第一次取操作执行成功后,银行卡里剩下50元,在第二次操作到来之前,小明妈妈往小明卡里转入50元;
  • 第二次操作来临时,发现卡里余额100元,扣除50元,此时卡里还剩50元
  • 小明血亏~~~

虽然P1以为内存地址中的变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free的算法中的CAS,因为CAS判断的是指针的地址。如果这个地址被重用,问题就很大了。

为解决ABA为题,我们可以采用具有原子性的内存引用计数等等办法。

解决ABA问题

方法一:维基百科上给了一个解--使用double-CAS (双保险的CAS)

例如,在32位系统上,我们要检查64位的内容.一次用CAS检查双倍长度的值,前半部是指针,后半部分是一个计数器。只有这两个都一样,才算通过检查。

要给内存V处赋新的值,需要并把计数器累加1。这样一来,ABA发生时,虽然值一样,但是计数器就不一样(但是在32位的系统上,这个计数器会溢出回来又从1开始的,这还是会有ABA的问题)

方法二:我们可以在V对象上加上一个版本号,取V对象的时候连版本号也取出来,当V对象每次被修改的时候都将V的版本号进行改变,那样就可以知道V对象有没有被修改过。

原子操作

代码最终都会被翻译为CPU指令,一条最简单加减法语句都有可能会被翻译成几条指令执行;

为了避免语句在CPU这一层级上的指令交叉带来的行为不可知,在多线程程序设计时我们必须通过一些方式来进行规范;这里面最常见的做法就是引入互斥锁,但互斥锁是操作系统这一层级的,最终映射到CPU上也是一堆指令,是指令就必然会带来额外的开销;

既然CPU指令是多线程不可再分的最小单元,那我们如果有办法将代码语句和指令对应起来,不就不需要引入互斥锁从而提高性能了吗?而这个对应关系就是所谓的原子操作;

自旋锁

使用原子操作模拟互斥锁的行为就是自旋锁,常用的自旋锁模型有:

TAS, Test-and-set,/* 有且只有atomic_flag类型与之对应 */

CAS, Compare-and-swap,
/*对应atomic的compare_exchange_strong 和 compare_exchange_weak,
这两个版本的区别是:Weak版本如果数据符合条件被修改,其也可能返回false,
就好像不符合修改状态一致;而Strong版本不会有这个问题,但在某些平台上
Strong版本比Weak版本慢;绝大多数情况下,我们应该优先选择使用Strong版本;*/

自旋锁:是指当一个线程在获取锁的时候,如果已经被其它线程获取,那么该线程将循环等待,然后不断判断锁是否能够被获取成功,知道获取到锁才会退出循环,如果不引入中断机制,会有大量计算资源浪费到轮询本身上;

对于互斥锁如果资源已经被占用,资源申请者只能进入睡眠状态。

参考:C++并发编程 | CAS的基本原理剖析(无锁编程、无锁数据结构)

你可能感兴趣的:(C++,开发语言)