C++11:原子交换函数compare_exchange_weak和compare_exchange_strong
C++11中引入了mutex和lock_guard。方便进行加锁解锁操作,但是如果想要性能更高的无锁实现,则可以使用原子操作 Atomic,我们可以利用它巧妙地实现无锁同步。
进行atomic学习前,需要明白一个概念:
CAS(Compare and Swap, 比较并替换)当值为预期值的时候,就将该值替换为预期的值。
此外,CAS也可以表述为:compareAndSet:比较并设置;compareAndExchange:比较并交换
这里使用 JAVA AtomicInteger 为例,来说明一下 CAS 。
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019));//true
System.out.println(atomicInteger.compareAndSet(5, 2020));//false
AtomicInteger 的初始值为 5,进行第一次 CAS的时候,替换成功,将 atomicInteger 的值替换为 2019,再进行第二次 CAS的时候,期望值为 5,但是此时 atomicInteger 的值为 2019,因此 cas 失败,返回 false 。
CA S最大的一个缺点就是:CAS 有自旋锁,即线程一直在执行逻辑,也就是一直让CPU进行计算操作。
CAS(Compare and Swap)是个原子操作,保证了如果需要更新的地址没有被他人改动多,那么它可以安全的写入。而这也是我们对于某个数据或者数据结构加锁要保护的内容,保证读写的一致性,不出现dirty data。现在几乎所有的CPU指令都支持CAS的原子操作。
Atomic
C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量值时,永远需要先获取修改前和修改后的值,而不会获得修改值过程中的数值。
进行原子操作的类都禁用了拷贝构造函数,原因是:原子读与原子写是2个独立的原子操作,无法保证2个独立操作加在一起是仍然保证原子性。
atomic提供了常见且容易理解的方法:
atomic提供了常见且容易理解的方法:
- store:原子写操作
- load:原子读操作
- exchange:允许两个数值进行交换,并保证整个过程是原子的;
- compare_exchange_weak与compare_exchange_strong:符合CAS(compare and set)概念,参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
compare_exchange_strong:atomic库中的一个函数,入参是3个,expect,desire,memory order,意思是如果当前的变量this的值等于expect值,则将this值改为desire,并返回true,否则,返回false,不进行修改,即进行一个读的操作。通常用于例如线程B等待线程A执行完毕,或者执行到某个步骤。此时线程B可以进行while等待,线程A在执行到对应步骤,将对应的原子变量置为expect值即可。类似于“接力运动”。这里由于会进行读写操作,所以,memory order一般是acq rel,而A线程由于要保证都执行完毕,执行顺序没有关系,所以一般是Release的memory order。
compare_exchange_weak和compare_exchange_strong
C++11中CAS实现:
template< class T>
struct atomic<T*>
{
public:
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure );
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile;
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst );
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure );
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst );
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile;
...
};
当前值与期望值(expect)相等时,修改当前值为设定值(desired),返回true
当前值与期望值(expect)不等时,将期望值(expect)修改为当前值,返回false
weak版和strong版的区别:
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。
实例:
-
在非并发条件下,要实现一个栈的Push操作,我们可能有如下操作:
- 新建一个节点
- 将该节点的next指针指向现有栈顶
- 更新栈顶
但是在并发条件下,上述无保护的操作明显可能出现问题。下面举一个例子:
- 原栈顶为A。(此时栈状态: A->P->Q->…,我们约定从左到右第一个值为栈顶,P->Q代表p.next = Q)
- 线程1准备将B压栈。线程1执行完步骤2后被强占。(新建节点B,并使 B.next = A,即B->A)
- 线程2得到cpu时间片并完成将C压栈的操作,即完成步骤1、2、3。此时栈状态(此时栈状态: C->A->…)
- 这时线程1重新获得cpu时间片,执行步骤3。导致栈状态变为(此时栈状态: B->A->…)
结果线程2的操作丢失,这显然不是我们想要的结果。
那么我们如何解决这个问题呢?
只要保证步骤3更新栈顶时候,栈顶是我们在步骤2中获得顶栈顶即可。因为如果有其它线程进行操作,栈顶必然改变。
我们可以利用CAS轻松解决这个问题:如果栈顶是我们步骤2中获取顶栈顶,则执行步骤3。否则,自旋(即重新执行步骤2)。
template<typename T>
class lock_free_stack
{
private:
struct node
{
T data;
node* next;
node(T const& data_):
data(data_)
{}
};
std::atomic<node*> head;
public:
void push(T const& data)
{
node* const new_node=new node(data);
new_node->next=head.load(); //如果head更新了,这条语句要重来一遍
while(!head.compare_exchange_weak(new_node->next,new_node));
}
};
在push方法里,atomic_compare_exchange_weak如果失败,则证明有其他线程更新了栈顶,而这个时候被其他线程更新的新栈顶值会被更新到new_node->next中,因此循环可以再次尝试压栈,而不需要程序更新new_node->next。