标签:
本文是无锁同步系列文章的第一篇,主要探讨C++11中的Atomic。
我们知道在C++11中引入了mutex和方便优雅的lock_guard。但是有时候我们想要的是性能更高的无锁实现,下面我们来讨论C++11中新增的原子操作类Atomic,我们可以利用它巧妙地实现无锁同步。
1 #include <thread>
2 #include <mutex>
3
4 #include <iostream>
5
6 using namespace std;
7
8 mutex g_mutex;
9 int g_count = 0;
10
11 int main()
12 {
13 thread thr1([]() {
14 for (int i = 0;i < 5;i++) {
15 lock_guard<mutex> lock(g_mutex); //①
16 g_count += 10;
17 }
18 });
19
20 thread thr2([]() {
21 for (int i = 0;i < 5;i++) {
22 lock_guard<mutex> lock(g_mutex); //②
23 g_count += 20;
24 }
25 });
26
27 thr1.join();
28 thr2.join();
29
30 cout << g_count << endl;
31
32 33 }
在上述例子中,如果把①②的锁注释后,我们可能无法得到正确的结果。原因是C++并没有给我们保证+=操作具有原子性(其本质应该是读-加-写3个操作)。
C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。
这些类都禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。
这些类中,最简单的是atomic_flag(其实和atomic<bool>相似),它只有test_and_set()和clear()方法。其中,test_and_set会检查变量的值是否为false,如果为false则把值改为true。
除了atomic_flag外,其他类型可以通过atomic<T>获得。atomic<T>提供了常见且容易理解的方法:
其中,compare_exchange_weak和compare_exchange_strong则是著名的CAS。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。但它比起strong有更高的性能。
下面举个简单的例子说明一下CAS的用途。
这个例子从《C++并发编程》摘抄而来,这是一个支持并发访问的栈,但却没有使用锁。因而具备非常高的性能。我们把这称之为无锁栈。
1 template<typename T>
2 class lock_free_stack
3 {
4 private:
5 struct node
6 {
7 std::shared_ptr<T> data;
8 std::shared_ptr<node> next;
9 node(T const& data_):
10 data(std::make_shared<T>(data_))
11 {}
12 };
13
14 std::shared_ptr<node> head;
15 public:
16 void push(T const& data)
17 {
18 std::shared_ptr<node> const new_node=std::make_shared<node>(data);
19 new_node->next=head.load();
20 while(!std::atomic_compare_exchange_weak(&head,
21 &new_node->next,new_node));
22 }
23 std::shared_ptr<T> pop()
24 {
25 std::shared_ptr<node> old_head=std::atomic_load(&head);
26 while(old_head && !std::atomic_compare_exchange_weak(&head,
27 &old_head,old_head->next));
28 return old_head ? old_head->data : std::shared_ptr<T>();
29 }
30 };
这段代码有很多巧妙之处。
在push方法里,atomic_compare_exchange_weak如果失败,证明有其他线程更新了栈顶,而这个时候被其他线程更新的新栈顶值会被更新到new_node->next中,因此循环可以直接再次尝试压栈。
在pop方法里,如果有2条线程同时执行到25行。
线程1使用完毕后对data进行delete,这是第二条线程仍然在执行第26、27行,如果没有old_head是否为空的判断,old_head->next可能会导致无法预料的访问。
标签:
原文地址:http://www.cnblogs.com/dengzz/p/5686866.html