0. 简单应用
所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
#include<thread>
#include<atomic>
#include<iostream>
using namespace std;
atomic_long total(0);
void click(int j) {
cout << "thread:" << j << endl;
for(int i = 0; i < 1000; ++i) {
total +=1;
}
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(click, i);
}
for (auto &t : threads) {
t.join();
}
cout << "value:" << total << endl;
}
1. 高阶用法
C++11给我们带来的Atomic一系列原子操作类,它们提供的方法能保证具有原子性。这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。
这些类都禁用了拷贝构造函数,原因是原子读和原子写是2个独立原子操作,无法保证2个独立的操作加在一起仍然保证原子性。
这些类中,最简单的是atomic_flag(其实和atomic相似),它只有test_and_set()和clear()方法。其中,test_and_set会检查变量的值是否为false,如果为false则把值改为true。
除了atomic_flag外,其他类型可以通过atomic获得。atomic提供了常见且容易理解的方法:
- store
- load
- exchange
- compare_exchange_weak
- compare_exchange_strong
store是原子写操作,而load则是对应的原子读操作。
exchange允许2个数值进行交换,并保证整个过程是原子的。
而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。
2 例子
下面举个简单的例子,使用CAS操作实现一个不带锁的并发栈。这个例子从《C++并发编程》摘抄而来。
Push
在非并发条件下,要实现一个栈的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)。
因此,不带锁的压栈Push操作比较简单。
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();
while(!head.compare_exchange_weak(new_node->next,new_node));
}
};