前言
多线程的一个重要问题就是并发,但是这个并发不是简单的一个线程访问A段内存,一个线程访问B段线程,这样显然不会出现任何问题。但是一旦两个线程同时访问同一段内存就会出现未知的错误。在前面讲过了使用互斥元的方法,可以保证只有一个线程能访问易发生冲突的内存区域。另外一种方法就是使用原子变量同步。
atomic
原子类型在头文件<atomic>中,使用atomic有两套命名模式:一种是使用替代名称,一种是使用atomic的特化。
| 原子类型 | 对应特化 |
|---|---|
| atomic_bool | atomic< bool > |
| atomic_char | atomic< char > |
| atomic_int | atomic< int > |
| … | … |
在同一个程序中不要混用两种方式,容易出现代码不可移植。
通常atomic仅限一下这几种操作:load(),store(),exchange(),compare_exchange_weak(),compare_exchange_strong()。
但是对于其中特殊的atomic_flag必须使用ATOMIC_FLAG_INIT进行初始化,只能有三类操作,分别是销毁、清除和设置并查询,对应的就是析构函数、clear()函数以及test_and_set()函数。并且atomic_flag不允许拷贝构造和拷贝赋值。
下面看一个例子:
class my_mutex {
atomic_flag flag;
public:
my_mutex():flag(ATOMIC_FLAG_INIT){}
void lock(){
while (flag.test_and_set(std::memory_order_acquire));
}
void unlock(){
flag.clear(memory_order_release);
}
};
my_mutex mx;
void fun() {
mx.lock();
cout << "hello" << endl;
mx.unlock();
}
这里使用了clear清除之前在里面存储的值,而test_and_set()则是一直查询里面的值,然后返回,如果返回的是true则继续循环,直到为false时才重新设置。
通过原子变量来实习互斥量的功能,但是这里阻塞是用的循环,直到循环条件不满足时才退出。里面的memory_order_ 在下一讲来慢慢展开。
除了atomic_flag之外其他的类都拥有其他的成员函数,下面就来依次介绍。
成员函数
load()
加载原子对象中存入的值,等价与直接使用原子变量。
atomic<bool> b;
bool x = b.load(memory_order_acquire);
store()
存储一个值到原子对象,等价于使用等号。
b.store(true);
看一下下面这两个例子:
void fun_2(){
atomic<int> b;
b.store(10);
cout << b.load() << endl;
b = 50;
cout << b << endl;
}
而且还可以添加内存模型参数,下面就是执行结果:
exchange()
返回原来里面存储的值,然后在存储一个新的值,相当于将上面两个load()和store()合成起来的参数。
void fun_2(){
atomic<int> b;
b.store(10);
cout << b.exchange(100) << endl;
cout << b;
}
等价于
void fun_3(){
cout << b.load();
b.store(100);
但是exchange()是作为一个原子操作,而下面两个单独的组合却是两个的原子操作的组合,不再是原子操作。
compare_exchange_weak()
交换-比较操作是比较原子变量值和所提供的期望值,如果二者相等,则存储提供的期望值,如果不等则将期望值更新为原子变量的实际值,更新成功则返回true反之则返回false。
atomic<bool> b;
b.compare_exchange_weak(expected, new_value);
当存储的值和expected相等时则将则更新为new_value,如果不等时则不变。其中expected必须是类型变量,而不能是常量。
void fun_3(){
atomic<int> b;
b = 20;
int a = 20;
bool x = b.compare_exchange_weak(a, 30);
cout << "第一次交换成功?" << x << " b = " << b << endl;
a = 300;
x = b.compare_exchange_weak(a, 30);
cout << "第二次交换成功?" << x << " b = " << b << endl;
}

测试结果也印证了前面的介绍。
compare_exchange_weak可能出现即使原始值和期望值相等时,也有可能出现存储不成功,这种情况下变量的值将不会改变,并且返回false。 这就是 伪失败
compare_exchange_strong()
不像compare_exchange_weak,此版本必须始终true在预期确实与所包含的对象相等时返回,不允许出现虚假故障。但是,在某些计算机上,对于某些在循环中进行检查的算法,compare_exchange_weak 可能会明显改善性能。
其余使用方法和compare_exchange_weak完全一致。
其他
针对std::atomic<T*>还有特殊操作,就是指针的移位操作。基本操作由fetch_add()和fetch_sub()提供,可以提供在存储地址上原子加减法操作。
还有就是fetch_or()、fetch_and()、fetch_xor(),以及对应的+=,-=,|=,&=,^=,++,–。都是可以实现的。
总结
这是使用成员函数的方法,还有一种是使用自由函数,即自由函数(&原子变量,相关参数);,这是为了和C语音兼容,常见命名规则就是在原有成员函数的基础上加上一个atomic_头即可。
可用的原子操作有载入(load)、存储(store)、交换(exchange)和比较/交换(compare & exchange),而且支持第一个参数使用shared_ptr<>。