atomic是c++11推出的原子变量,使用需要C++11及更高标准,包含的头文件为#include
atomic可以声明基本类型的变量,如下:
std::atomic a(0);
std::atomic b('0');
需要注意的是atomic变量不支持拷贝,因此我们不能让一个atomic变量等于另一个atomic变量。但是atomic可以使用=来将对应的基本类型赋值。下面是一些例子:
std::atomic a = 0; //错误,禁用了拷贝构造函数,构造时无法使用=
std::atomic a(0); //正确
std::atomic b = a; //错误,禁用了拷贝构造函数
std::atomic b(a); //错误,同上
std::atomic b(0); //正确
a=1; //正确
b=a; //错误,禁用了拷贝=
atomic也支持自定义类型,但是并不支持所有的自定义类型。如果自定义类型在以下表达式的值均为true方可生成atomic变量:
auto ret = std::is_trivially_copyable::value;
ret = std::is_copy_constructible::value;
ret = std::is_move_constructible::value;
ret = std::is_copy_assignable::value;
ret = std::is_move_assignable::value;
只有这五个表达式均为真才能生成该自定义结构的atomic变量。因此,自定义数据类型必须有拷贝赋值运算符、不能有任何虚函数或虚基类、必须使用编译器创建的拷贝赋值操作、该类型中所有的基类和非静态数据成员都需要支持拷贝赋值操作以及必须是“位可比的”。简单的说自定义类型如果可以使用memcpy来赋值并可以使用memcmp来按位比较。
不过使用自定义类型如果需要对其原子化我们可以根据需要让它使用原子变量作成员变量。
大多数原子变量是对数据进行无锁操作即可实现线程安全,但是针对一些变量是无法通过无锁操作来实现线程安全的,此时为保证变量可以实现原子操作,系统给它们内置了锁。而is_lock_free可以判断该变量是否是无锁的。大多数自定义数据类型都无法实现无锁保证原子性。
首先原子类型和原类型不是同一个类型,因此我们需要使用原子类型作参数时应该如下写法:
int func1(int a); //普通int
int func2(std::atomic a); //原子类型atomic做参数
int func3(std::atomic& a); //引用传递
std::atomic a;
func1(a); //错误
func2(a); //错误
func3(a); //正确
根据上面我们发现当原子类型作引用传递时可以正常运行,但是作形参时无法成功运行。原因很简单,前面我们已经说过atomic变量不支持拷贝,同样的也没有拷贝构造函数,而形参会根据参数构建临时对象,需要用到拷贝,因此原子类型无法作形参。实际上很多编译器会优化函数使部分函数不产生不必要的临时对象,但是从语法角度来讲是不可以这样的。
我们可以通过load得到其对应的类型的实体,如下:
std::atomic a(5);
std::cout<
store可以设定原子变量实体的值,如下:
std::atomic a(5);
a=6; //此时a=6
a.store(7); //此时a=7
实际上原子类型重载的=用的也是store函数,源码如下:
__int_type operator=(__int_type __i) noexcept
{
store(__i);
return __i;
}
但是store除了值之外还有一个参数,如下:
store(__int_type __i, memory_order __m = memory_order_seq_cst) noexcept
我们发现还有第二个参数,而重载的=里面的store只有第一个参数,第二个用的是默认值。我们再看看load函数:
load(memory_order __m = memory_order_seq_cst) const noexcept
我们发现它也有一个默认参数,而我们前面调用时并没有使用。实际上这是控制原子操作的内存顺序的选项,我们下一章将会讲解。而我们使用load和store函数而非=也与这个有关。
我们写一个简单的程序来观察结果:
#include
#include
#include
#include
int func1(std::atomic& a)
{
for (int i = 0; i < 1000 * 100; i++) {
a += 1;
}
}
int func2(int& b)
{
for (int i = 0; i < 1000 * 100; i++) {
b += 1;
}
}
int main()
{
std::atomic a(0);
int b = 0;
std::thread t1(func1, std::ref(a));
std::thread t2(func1, std::ref(a));
std::thread t3(func2, std::ref(b));
std::thread t4(func2, std::ref(b));
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << a << ' ' << b << '\n';
return 0;
}
输出结果为:
可以看出原子变量是线程安全的,而普通变量不是。
在我们编译程序时,程序执行顺序和代码顺序并不一定一致,先看下面程序:
int func(int a) {
// 第一部分
int b = a; // 1
int c = b+1; // 2
int d = c+1; // 3
// 第二部分
int x = a; // 4
int y = x+1; // 5
int z = y+1; // 6
}
代码顺序是123456,但是实际上并不一定是这个顺序。根据编译器优化等级可能第一部分和第二部分顺序会不一致。首先看第一部分,第2行b+1依赖于第1行的结果,第三行c+1依赖第二行的结果,因此这三行之间顺序不会变。同理,第二部分之中的456顺序不会变。但是我们发现第二部分中每行均不依赖与第一部分,所以每行都有可能出现在第一部分的任意位置之前。比如456123,124563,412536等,只要满足1,2,3之间顺序不变,4,5,6之间顺序不变均可能产生。
上述内容对于单线程不会有影响,但是对于多线程可能会产生意外的结果,如下:
int a(0),b(0);
void func1()
{
a=1;
b=2;
}
void func2()
{
std::cout<
如果我们不知道内存乱序,那么第二个结果可能有三种:
a.func2在a=1之前运行完毕,输出{0,0};
b.func2在a=1之后b=2之前执行完毕,输出{1,0};
c.func2在b=2之后执行完毕,输出{1,2};
显然如果顺序执行不会出现{0,2},因为顺序执行的话b赋值完成时a一定等于1,但是由于它是乱序执行,因此可能产生这种结果。
多线程出现内存乱序主要原因是在于多核cpu执行指令时,当某一线程的一条指令执行完毕修改了值,该值可能存在某个CPU的缓存中,而其他CPU的缓存还是原来的值,这就导致读写不一致。
对于避免乱序可以加互斥锁,不过互斥锁用在应对乱序上那么性能方面会大幅损耗,而原子变量提供的6种内存模型可以在性能开销相对较小的情况下保证顺序。
原子类型有6种内存操作选项,分别是:
1.memory_order_relaxed
2.memory_order_consume
3.memory_order_acquire
4.memory_order_release
5.memory_order_acq_rel
6.memory_order_seq_cst
而我们在1.3中发现了load和store的默认参数是memory_order_seq_cst。所有的atomic类型内置的函数,如果可以选择操作选项却没有选择,那么默认便是memory_order_seq_cst。而内置的重载运算符如=,+,-由于未传递任何参数,因此它们都是以默认的memory_order_seq_cst方式调用相应函数。
这六种内存操作选项代表三种内存模型,分别是:排序一致序列(memory_order_seq_cst),获取-释放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel)和松散序列(memory_order_relaxed)。
我们先来看看这两个相对极端的序列。排序一致性序列,即大多数方法默认的memory_order_seq_cst。排序一致性序列是对原子变量最强的约束,它会使一个多线程程序以某种特殊的排序执行,就像单线程一样。它是最安全的方式。不过安全带来的副作用是性能的损耗。松散序列即memory_order_relaxed,它只保证此次操作是原子的。看看下列代码:
std::atomic x(false),y(false);
void write_x_then_y()
{
x.store(true,std::memory_order_relaxed); // 1
y.store(true,std::memory_order_relaxed); // 2
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed)); // 3
assert(x.load(std::memory_order_relaxed)); // 4
}
如果按顺序执行则4不会触发,但是由于使用的是std::memory_order_relaxed,那么有可能出现2->3->4->1的顺序,但是它仍时原子操作。我们看下面代码:
void func1()
{
for (int i = 0; i < 1000 * 100; i++) {
x.fetch_add(1, std::memory_order_relaxed);
}
}
void func2()
{
for (int i = 0; i < 1000 * 100; i++) {
x.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}
最后我们可以的到200000,这是正确值,因为即便是松散序列每个操作也是原子的。
获取释放序列最常用的是这两个:memory_order_release和memory_order_acquire。memory_order_acquire在读取时可以加此参数,其之后的操作不能重排到该指令之前,memory_order_release在写入时可以加此参数,之前的操作不能排在该操作之后。执行memory_order_acquire类似互斥锁的lock,memory_order_release类似unlock,示例如下:
std::atomic x(false),y(false);
void write_x_then_y()
{
x.store(true,std::memory_order_relaxed); // 1
y.store(true,std::memory_order_release); // 2
}
void read_y_then_x()
{
while(!y.load(std::memory_order_acquire)); // 3
assert(x.load(std::memory_order_relaxed)); // 4
}
此时执行第四步的断言时x一定为true。
memory_order_consume和memory_order_acquire类似,但是它放宽了要求,其表示在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前,如下代码:
std::atomic x(false);
std::atomic y;
y=new bool(false);
void write_x_then_y()
{
x.store(true,std::memory_order_relaxed); // 1
y.store(true,std::memory_order_release); // 2
}
void read_y_then_x()
{
while(!y.load(std::memory_order_consume)); // 3
assert(x.load(std::memory_order_relaxed)); // 4
}
此时 1有可能被乱序到2后面,导致触发4断言。一般不建议使用这个,因为较高几率出错导致带来的代价一般要高于在此节省的效率成本。
memory_order_acq_rel相当于memory_order_release和memory_order_acquire的结合,同时约束该语句前面语句和后面语句的顺序。
对非原子操作和原子操作混用时,可以将非原子操作近似看成memory_order_relaxed,因此当含有顺序一致性序列和获取释放队列时也会隔离非原子操作。
CAS是一种常见的无锁算法,其流程简单概括如下:
do {
备份旧数据;
构造新数据;
}while(!CAS(内存地址,旧数据,新数据))
其中CAS将比较旧数据和内存数据是否相同,如果相同,我们认为这个值没有被改变,所以可以写入新值,反之则说明在构造新数据时已经有其它线程把其它新值写入内存地址,此时则重新进行备份和构造操作。
上述操作仅判断了是否相对,但是引入了ABA问题即另一个线程可能会把变量的值从A改成B,又从B改回成A。很多情况下,ABA问题不会影响程序逻辑,此时可以忽略。但有时不能忽略。一般的做法是给变量关联一个只能递增、不能递减的版本号,在CAS比较时再比较版本号。
atomic提供了两个CAS函数,compare_exchange_strong 与 compare_exchange_weak。使用方法如下:
void func1()
{
for (int i = 0; i < 100 * 1000; i++) {
int x1 = x;
while (!x.compare_exchange_weak(x1, x + 1));
}
}
void func2()
{
for (int i = 0; i < 100 * 1000; i++) {
int x1 = x;
while (!x.compare_exchange_strong(x1, x + 1));
}
}
其中weak里x的内存和x1相等时有可能返回false,但是效率比strong快的多。此外使用循环可以避免weak产生的问题。在x86平台weak不会发生此问题。
实现简单的自旋锁代码如下:
int a(0);
class SpinLock {
public:
SpinLock() : flag(false)
{}
void lock()
{
bool expect = false;
while (!flag.compare_exchange_weak(expect, true, std::memory_order_relaxed))
{
expect = false;
}
}
void unlock()
{
flag.store(false, std::memory_order_relaxed);
}
private:
std::atomic flag;
};
SpinLock spinLock;
void func1()
{
for (int i = 0; i < 1000 * 1000 * 100; i++) {
spinLock.lock();
a++;
spinLock.unlock();
}
}
void func2()
{
for (int i = 0; i < 1000 * 1000 * 100; i++) {
spinLock.lock();
a++;
spinLock.unlock();
}
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t2.join();
t1.join();
std::cout<
最后可以得到正确的结果。下面实现一个类似lock_guard的:
int a(0);
class SpinLock {
public:
SpinLock() : flag(false)
{}
void lock()
{
bool expect = false;
while (!flag.compare_exchange_weak(expect, true, std::memory_order_relaxed))
{
expect = false;
}
}
void unlock()
{
flag.store(false, std::memory_order_relaxed);
}
private:
std::atomic flag;
};
SpinLock spinLock;
class SpinLockGuard {
public:
SpinLockGuard(SpinLock& spinlock) : spinLock(spinlock) {
spinLock.lock();
}
~SpinLockGuard() {
spinLock.unlock();
}
protected:
SpinLock& spinLock;
};
void func1()
{
for (int i = 0; i < 1000 * 1000 * 10; i++) {
SpinLockGuard lck(spinLock);
a++;
}
}
void func2()
{
for (int i = 0; i < 1000 * 1000 * 10; i++) {
SpinLockGuard lck(spinLock);
a++;
}
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t2.join();
t1.join();
std::cout<
这样我们就可以实现简单的自旋锁了。