本文是无锁同步系列文章的第一篇,主要探讨C++11中的Atomic。
我们知道在C++11中引入了mutex和方便优雅的lock_guard。但是有时候我们想要的是性能更高的无锁实现,下面我们来讨论C++11中新增的原子操作类Atomic,我们可以利用它巧妙地实现无锁同步。
#include#include #include using namespace std; mutex g_mutex; int g_count = 0; int main() { thread thr1([]() { for (int i = 0;i < 5;i++) { lock_guard lock(g_mutex); //① g_count += 10; } }); thread thr2([]() { for (int i = 0;i < 5;i++) { lock_guard lock(g_mutex); //② g_count += 20; } }); thr1.join(); thr2.join(); cout << g_count << endl; }
在上述例子中,如果把①②的锁注释后,我们可能无法得到正确的结果。原因是C++并没有给我们保证+=操作具有原子性(其本质应该是读-加-写3个操作)。
C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。
这些类都禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。
这些类中,最简单的是atomic_flag(其实和atomic
除了atomic_flag外,其他类型可以通过atomic
store是原子写操作,而load则是对应的原子读操作。
exchange允许2个数值进行交换,并保证整个过程是原子的。
而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。
下面举个简单的例子,使用CAS操作实现一个不带锁的并发栈。这个例子从《C++并发编程》摘抄而来。
在非并发条件下,要实现一个栈的Push操作,我们可能有如下操作:
但是在并发条件下,上述无保护的操作明显可能出现问题。下面举一个例子:
结果线程2的操作丢失,这显然不是我们想要的结果。
那么我们如何解决这个问题呢?只要保证步骤3更新栈顶时候,栈顶是我们在步骤2中获得顶栈顶即可。因为如果有其它线程进行操作,栈顶必然改变。
我们可以利用CAS轻松解决这个问题:如果栈顶是我们步骤2中获取顶栈顶,则执行步骤3。否则,自旋(即重新执行步骤2)。
因此,不带锁的压栈Push操作比较简单。
templateclass lock_free_stack { private: struct node { T data; node* next; node(T const& data_): data(data_) {} }; std::atomic head; public: void push(T const& data) { node* const new_node=new node(data); new_node->next=head.load(); while(!head.compare_exchange_weak(new_node->next,new_node)); } }; 我们可以注意到一个非常巧妙的设计。在push方法里,atomic_compare_exchange_weak如果失败,证明有其他线程更新了栈顶,而这个时候被其他线程更新的新栈顶值会被更新到new_node->next中,因此循环可以直接再次尝试压栈而无需由程序员更新new_node->next。
----------------------------------
bool compare_exchange_weak (T& expected, T val, ..);
但令我感到困惑的是C ++ 11标准(ISO / IEC 14882)中所说的:
29.6.5 ..虚假失效的后果是几乎所有使用弱比较交换的应用都将处于循环中。
另一个问题有关。在他的书“C ++ Concurrency In Action”中:
//Because compare_exchange_weak() can fail spuriously, it must typically //be used in a loop: bool expected=false; extern atomicb; // set somewhere else while(!b.compare_exchange_weak(expected,true) && !expected); //In this case, you keep looping as long as expected is still false, //indicating that the compare_exchange_weak() call failed spuriously.
为什么!expected
在循环条件?是否有阻止所有线程在一段时间内挨饿并且没有进展的问题?
// use LL/SC (or CAS on x86) and ignore/loop on spurious failures while (!compare_exchange_weak(..)) { .. } // use LL/SC (or CAS on x86) and ignore/loop on spurious failures while (!compare_exchange_strong(..)) { .. }
通常情况下,你希望你的工作在你继续前完成,因此,你把它compare_exchange_weak
放到一个循环中,以便它试图交换,直到它成功(即返回true
)。
请注意,这也compare_exchange_strong
经常用于循环。它不会因虚假故障而失败,但由于并发写入而失败。
weak
而不是strong
?很容易:虚假失败并不经常发生,所以它没有大的表现。相反,容忍这样的故障允许在某些平台上更有效地实现weak
版本(相比strong
):strong
必须始终检查虚假故障并掩盖它。这很贵。
因此,weak
它被使用是因为它比strong
在某些平台上快得多
weak
什么时候strong
?该参考指出提示何时使用weak
,何时使用strong
:
当比较和交换处于循环中时,弱版本将在某些平台上产生更好的性能。当一个弱的比较和交换需要一个循环,而一个强大的循环不会,强壮的一个更可取。
所以答案似乎很容易记住:如果仅仅因为虚假故障而不得不引入一个循环,那么不要这样做; 使用strong
。如果你有一个循环,然后使用weak
。
!expected
在这个例子中它取决于情况及其期望的语义,但通不需要正确性。忽略它会产生非常类似的语义。只有在另一个线程可能重置值的情况下false
,语义可能会略有不同(但我找不到一个有意义的示例)。有关详细解释,请参阅Tony D.的评论。
当另一个线程写入时,它只是一个快速通道true
:然后我们放弃而不是试图true
再次写入。