原子操作:CAS、TAS、TTAS、FAA浅析

原子操作:CAS、TAS、TTAS、FAA浅析

什么是原子操作

原子操作(atomic operation)是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。保证访问共享资源是唯一的。

一般原子操作需要CPU等硬件支持,使用硬件同步原语来实现原子操作。常用的原语有CAS(compare-and-swap)TES(test-and-set)FAA(fetch-and-add)

CAS

什么是CAS

在计算机科学中,比较与交换(CAS)是多线程中用来实现同步的原子指令。它将内存位置的内容与给定值进行比较,只有当它们相同时,才将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的

// wiki的伪代码
function cas(p : pointer to int, old : int, new : int) returns bool {
    if *p ≠ old {
        return false
    }
    *p ← new
    return true
}

当内存值发生变化后,一直使用cas原语去尝试更新值时,叫做自旋。

cas的存在的问题

ABA problem

wiki ABA问题
ABA问题可以简单的转换为,线程或者进程两次读取相同的值以后是否关注在两次读取之前变量的值是否发送改变,如果中间的改变会影响计算结果,就会出现ABA问题,如果只关注值本身,那么即使出现了ABA也不会影响程序执行结果。
如:
A 读取到 V的值 100 挂起(线程切换)
B 读取到V的值 100 继续执行,比较并交换后更新为40,退出
C 读取到V的值为40,比较并更新为100,退出
A 继续执行,因为V的值为100,A可以对V进行操作可以把V的值设置为70

以上例子可以看出发生了ABA的问题,但是对于以上案例来说,对程序执行结果并没有影响。

如:
S是一个链表 a->b->a->e
A 要获取到a结点想把a替换为f,预期结果为f->b->a->e->e,但是A被挂起了(线程切换)
B 要获取a以后删掉a结点,B执行成功 变为b->a->e
C获取到b结点以后删除来了b C执行成功 变为a->e
A 被唤醒继续执行 发现链表的头还是a,执行结果为 f->e 【实际不应该在继续更新的,因为此a非彼a】

以上例子可以看出来发生了ABA问题,ABA问题也导致了程序执行异常。因为无法保证现在的a还是之前的a,那么怎么保证现在的a是之前的a?通用的做法就是给打个标记(版本号)。那么怎么打标记呢?

前人也提供了很多解决方案:
1. 使用double-length CAS (DCAS).在32位系统上,可以使用64位CAS,64位的内存可以分为两个部分。一部分用于存放具体的值,一部分用于存放版本信息,这样就可以解决ABA的问题,当然在64的情况下,可以使用128位CAS。
2. 对于不支持DCAS的CPU,如果是32位系统,可以认为进行拆分,如,前16位用于版本信息,后16位用于对应的值。
3. 使用一个更复杂但更有效的解决方案是实施安全内存回收(SMR)(了解即可)

Costs and benefits

多线程反复尝试更新某一个变量,太多重试自旋,会消耗大量CPU资源,会给CPU带来很大的压力。
因此冲突如果过于频繁的场景不建议使用CAS原语进行处理(CAS也是乐观锁的机制,乐观锁不擅长冲突频繁的场景,这时候可以选择悲观锁的机制)

TAS

什么是TAS

在计算机科学中,test-and-set指令是一种用来将1 (set)写入一个内存位置,并以单个原子的形式返回其旧值的指令。,不可中断)操作。

// wiki 伪代码
function Lock(boolean *lock) { 
    while (test_and_set(lock) == 1); 
}

TAS特点是自旋,也就是循环,每次尝试去设置值,如果设置成功则会返回,如果没有返回就会一直自旋,知道设置成功值。此时进入临界区,执行完临界区数据,再设置bool变量为false。从而让其他线程拿到锁。

 volatile int lock = 0;
 void Critical() {
     while (TestAndSet(&lock) == 1);
     critical section // only one process can be in this section at a time
     lock = 0 // release lock when finished with the critical section
 }
TAS的问题

当使用TAS实现TASLock (Test And Set Lock)测试-设置锁,它的特点是自旋时,每次尝试获取锁时,底层还是使用CAS操作,不断的设置锁标志位的过程会一直修改共享变量的值(回写),会引发缓冲一致性流量发风暴。【因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。】

为什么回写会影响性能?

【因为每一次CAS操作都会发出广播通知其他处理器,通知的过程会出现总线锁定,从而影响程序的性能。】

TTAS

什么是TTAS

Test and test-and-set 用于在多处理器环境中实现互斥的指令。为了解决TAS过程中通过CAS不断修改共享变量而导致的性能问题,而提出的方案。

TTAS的特点也是自旋,主要是为了降低性能开销,主要思路是减少回写(CAS导致的不断修改共享变量的值)。第一步先去检测 lock是否空闲;不可用就让出CPU,可用就执行TAS进行CAS操作,只有一个线程先观测到lock是空闲的,该线程才会尝试的去获取它,从而消除一部分回写操作。

// wiki 
boolean locked := false // shared lock variable
procedure EnterCritical() {
  do {
    while (locked == true) yield(); // lock looks busy so yield to scheduler
  } while TestAndSet(locked) // actual atomic locking
}
procedure TestAndSet(lock) {
   boolean initial = lock;
   lock = true;
   return initial;
 }

FAA

什么是FFA

Fetch-and-add指令将存储位置的内容增加指定的值(原子操作)。其他线程无法观测中间结果。

FAA特点就是原子性,因为在多处理器的计算中进行x=x+a的操作,实际上有三步:

  1. 获取变量x的地址,把x的值放到寄存器中
  2. 在寄存器增加a
  3. 把寄存器的新值放到x的地址中。
    FAA的意义就是把这三步原子化,要么都执行要么都不执行。
// wiki
function FetchAndAdd(address location, int inc) {
    int value := *location
    *location := value + inc
    return value
}

FFA的存在的问题

FFA的使用场景比较局限(自增自建的场景如计数器。

参考资料

原子操作百度百科
聊聊高并发(六)实现几种自旋锁(一)

你可能感兴趣的:(并发,并发,原子操作,CAS,TAS,TTAS)