我们知道在C++11中引入了mutex和方便优雅的lock_guard。但是有时候我们想要的是性能更高的无锁实现,下面我们来讨论C++11中新增的原子操作类Atomic,我们可以利用它巧妙地实现无锁同步。
CAS(Compare and Swap)是个原子操作,保证了如果需要更新的地址没有被他人改动多,那么它可以安全的写入。而这也是我们对于某个数据或者数据结构加锁要保护的内容,保证读写的一致性,不出现dirty data。现在几乎所有的CPU指令都支持CAS的原子操作。
Atomic
C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。
这些类都禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。
atomic
- store
- load
- exchange
- compare_exchange_weak
- compare_exchange_strong
store是原子写操作,而load则是对应的原子读操作。
exchange允许2个数值进行交换,并保证整个过程是原子的。
而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
compare_exchange_weak和compare_exchange_strong
C++11中CAS实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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; ... }; |
当前值与期望值相等时,修改当前值为设定值,返回true
当前值与期望值不等时,将期望值修改为当前值,返回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)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 1 template<typename T> 2 class lock_free_stack 3 { 4 private: 5 struct node 6 { 7 T data; 8 node* next; 9 10 node(T const& data_): 11 data(data_) 12 {} 13 }; 14 15 std::atomic<node*> head; 16 public: 17 void push(T const& data) 18 { 19 node* const new_node=new node(data); 20 new_node->next=head.load(); //如果head更新了,这条语句要春来一遍 21 while(!head.compare_exchange_weak(new_node->next,new_node)); 22 } 23 }; |
我们可以注意到一个非常巧妙的设计。在push方法里,atomic_compare_exchange_weak如果失败,证明有其他线程更新了栈顶,而这个时候被其他线程更新的新栈顶值会被更新到new_node->next中,因此循环可以直接再次尝试压栈而无需由程序员更新new_node->next。