引言
前面章节介绍的st::mutex可以保证多线程之间数据访问的互斥性,但是C++ 11还提供了一种原子类型,即atomic,它提供了多线程间的原子操作,它是一种不需要用到mutex技术的多线程并发编程方式,一个操作一旦开始,这个操作就不能被处理器拆分处理,能够确保所有其他线程都不在同一时间访问该资源,不会存在数据竞争的问题。
所以对于原子操作来讲,是不可能看到原子操作只完成了部分这种情况的, 它要么是做了,要么就是没做,只有这两种可能。虽然mutex也可以提供共享资源的访问的保护,但是加锁一般针对一个代码段,atomic针对的一般都是一个变量;而且原子操作更加接近底层,因而效率一般比使用互斥量更高。可以说,原子操作轻松地化解了互斥访问共享数据的难题。
atomic原型
<atomic> 该头文主要声明了两个类,std::atomic 和 std::atomic_flag,其中实现了原子类型的所有特性。另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
-
C++ class
std::atomic是一个模板类,模板参数是数据类型。
template <class T>
struct atomic<T>
-
构造方法
既然是一个类,那肯定有对应的构造方法:
(1). 默认构造函数
构造一个未初始化的原子对象。(后面可通过调用atomic_init
进行初始化)
(2). 初始化构造函数
由类型 T初始化一个 std::atomic对象。比如: atomic<int> x(10);
(3). 复制构造函数 [禁用]
禁用复制构造函数,原子对象不可复制/移动。原因是按照定义,原子类型上的操作全部是原子化的,但拷贝赋值和拷贝构造都涉及两个对象,这个过程中,必须先从来源对象读取值,再将其写出到目标对象,这是在两个独立对象上的两个独立操作,其组合不保证还是原子化的。
生成一个T类型的原子对象,并提供了系列原子操作函数
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
int main(void) {
//默认构造函数
atomic<int> a;
atomic_init(&a,100);
//初始化构造函数
atomic<int> b(200);
return 0;
}
-
成员函数
atomic提供了一些与原子操作有关的成员函数。
成员函数 | 描述 |
store() | 存储一个值到原子对象,等价于使用等号。 |
load() | 加载原子对象中存入的值,等价与直接使用原子变量 |
exchange() | 返回原来里面存储的值,然后this会再存储新的值,相当于将上面两个load() 和store() 合成起来 |
compare_exchange_weak( T&expected, T desired,....) | 交换-比较操作是比较原子变量值和所提供的期望值,如果二者相等,则this存储提供的期望值desired,如果不等则将期望值expected更新为原子变量的实际值,更新成功则返回true 反之则返回false |
compare_exchange_strong (T& expected, T desired, ......) | 如果当前的变量this的值等于expected值时,则将this值改为desired,并返回true;否则,不对this的值进行修改,expected被赋值为this的值,并返回false. |
compare_exchange_strong和compare_exchange_weak有什么差别呢?weak版本可能会出现这种状况:即使原子变量的值等于期望值,保存动作还是有可能失败,在这种情形下,原子变量维持不变,compare_exchange_weak()返回false,这种现象称之为佯败(spurious failure); strong版不会出现weak版本中佯败现象,strong版本在预期值与this对象相等时必须始终返回true。
weak版本为什么会出现佯败呢?因为原子化的比较-交换必须由一条指令单独完成,而某些处理器没有这种指令,无法保证该操作按原子化方式完成。要实现比较-交换,负责的线程则必须改为连续运行一些列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败。这种计算机最有可能引发上述的保存失败,我们称之为佯败(spurious failure)。其败因不是变量值本身存在问题,而是函数执行时机不对。(摘自:【并发编程十三】c++原子操作(1)_郑同学的笔记的博客-CSDN博客)
weak版本允许偶然出乎意料的返回(即this的值和预期值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。waek版本在交换操作失败时不会抛出异常,而是返回一个bool值表示操作是否成功。
下面简单试验了一下前面所讲的函数
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> a;
int main(void) {
atomic<int> a;
//1. use store()
a.store(100);
cout << "\n 1. After a.store(100) :a = " << a << endl;
//2. use load()
int x = a.load();
cout << "\n 2. After x=a.load() : x = " << x << endl;
//3. use exchange()
int y = a.exchange(200);
cout <<"\n 3. a.exchange(200) will return original value of a :"<< y << endl;
cout <<"\n 4. After a.exchange(200), a="<< a << endl;
//4. usecompare_exchange_weak
int expected1 = 300;
int expected2 = 200;
cout << "\n 5. test compare_exchange_weak()\n";
cout <<" a = "<<a<<", expecte1 = "<<expected1 <<", desired = 400" << endl;
bool isSucess = a.compare_exchange_weak(expected1,400);
cout << " Is first exchage sucess: " << (isSucess? "Sucess":"Fail") << ". a = "<<a <<"; expeced1 = "<< expected1 << endl;
cout <<"\n a = " << a << ", expecte2 = " << expected2 << ", desired = 400" << endl;
isSucess = a.compare_exchange_weak(expected2, 400);
cout << " Is second exchage sucess: " << (isSucess ? "Sucess" : "Fail") << ". a = " <<a<<"; expeced2 = " << expected2 << endl;
//5. usecompare_exchange_strong
int expected3 = 500;
int expected4 = 400;
cout << "\n 6. test compare_exchange_strong()\n";
cout << " a = " << a << ", expecte3 = " << expected3 << ", desired = 500" << endl;
isSucess = a.compare_exchange_strong(expected3, 600);
cout << " Is first exchage sucess: " << (isSucess ? "Sucess" : "Fail") << ". a = " << a << "; expeced3 = " << expected3 << endl;
cout << "\n a = " << a << ", expecte4 = " << expected4<< ", desired = 400" << endl;
isSucess = a.compare_exchange_strong(expected4, 600);
cout << " Is second exchage sucess: " << (isSucess ? "Sucess" : "Fail") << ". a = " << a << "; expeced4 = " << expected4 << endl;
return 0;
}
-
特化成员函数
特化成员函数 | 描述 | 说明 |
fetch_add | 原子地以当前值和参数的算术加法结果替换掉当前值,并返回原始值。 | 适用于整形和指针类型的std::atomic 特化版本 |
fetch_sub | 原子地以当前值和参数的算术减法结果替换掉当前值,并返回原始值。 | 适用于整形和指针类型的std::atomic 特化版本 |
fetch_and | 原子地以当前值与参数进行 “与操作”, 并返回原始值,整个操作是原子性的 | 只适用于整形的原子对象 |
fetch_or | 原子地以当前值与参数进行 “或操作” ,并返回原始值,整个操作是原子性的 | 只适用于整形的原子对象 |
fetch_xor | 原子地以当前值与参数进行 “异或操作” ,并返回原始值,整个操作是原子性的 | 只适用于整形的原子对象 |
operator++ | 令原子值增加一,并返回生成的值 | 适用于整形和指针类型的std::atomic 特化版本 |
operator++(int) | 令原子值增加一,并返回之前的值 | 适用于整形和指针类型的std::atomic 特化版本 |
operator–- | 令原子值减一,并返回生成的值 | 适用于整形和指针类型的std::atomic 特化版本 |
operator–-(int) | 令原子值减一,并返回之前的值 | 适用于整形和指针类型的std::atomic 特化版本 |
operator+= | 适用于整形和指针类型的std::atomic 特化版本 | |
operator-= | 适用于整形和指针类型的std::atomic 特化版本 | |
operator&= | 只适用于整形的原子对象 | |
operator|= | 只适用于整形的原子对象 | |
operator^= | 只适用于整形的原子对象 |
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
constexpr auto THREAD_NUM = 10;
atomic<int> a;
void incNumber(int num) {
for (int i = 0; i < num; ++i) {
this_thread::sleep_for(chrono::milliseconds(1));
a.fetch_add(1);
}
}
int main() {
atomic_init(&a, 0);
thread th[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; ++i) {
th[i] = thread(incNumber, 100);
}
for (int i = 0; i < THREAD_NUM; ++i) {
th[i].join();
}
cout << " a = " << a << endl;
system("pause");
return 0;
}
atomic_flag
atomic_flag是一种简单的原子类型,表示一个布尔标志,该类型的对象只有两种状态:成立或置零。只支持两种操作:test_and_set
和clear
。
-
构造函数
atomic_flag() noexcept = default;
atomic_flag (const atomic_flag&T) = delete;
std::atomic_flag 只有默认构造函数,拷贝构造函数已被禁用,因此不能从其他的 std::atomic_flag 对象构造一个新的 std::atomic_flag 对象。
automic_flag内含又一个标志位,在使用之前使用宏ATOMIC_FLAG_INIT进行初始化,将其中的标志初始化为置零状态。无论在哪里声明、处于什么作用域,atomic_flag永远以置零状态开始。
-
操作方法
1. test_and_set()
检测其中的标志位,如果是0,就置位1,返回0;如果是1就不变,返回1,这些操作都是原子性的。
2. clear()
用于把标志位置位0。使用前面说的ATOMIC_FLAG_INIT对std::atomic_flag 对象进行初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。
-
说明
可以看出atomic_flag严格受限,无法用作普通的布尔标志,因此最好还是使用atomic<bool>
-
C-style atomic types
<atomic> also declares an entire set of C-style types and functions compatible with the atomic support in C. The following atomic types are also defined in this header; each with the same behavior as the respective instantiation of atomic for the listed contained type.
contained type | atomic type | description |
---|---|---|
bool | atomic_bool | |
char | atomic_char | atomics for fundamental integral types. These are either typedefs of the corresponding full specialization of the atomic class template or a base class of such specialization. |
signed char | atomic_schar | |
unsigned char | atomic_uchar | |
short | atomic_short | |
unsigned short | atomic_ushort | |
int | atomic_int | |
unsigned int | atomic_uint | |
long | atomic_long | |
unsigned long | atomic_ulong | |
long long | atomic_llong | |
unsigned long long | atomic_ullong | |
wchar_t | atomic_wchar_t | |
char16_t | atomic_char16_t | |
char32_t | atomic_char32_t | |
intmax_t | atomic_intmax_t | atomics for width-based integrals (those defined in <cinttypes>). Each of these is either an alias of one of the above atomics for fundamental integral types or of a full specialization of the atomic class template with an extended integral type. Where N is one in 8, 16, 32, 64, or any other type width supported by the library. |
uintmax_t | atomic_uintmax_t | |
int_least N_t | atomic_int_least N_t | |
uint_least N_t | atomic_uint_least N_t | |
int_fast N_t | atomic_int_fast N_t | |
uint_fast N_t | atomic_uint_fast N_t | |
intptr_t | atomic_intptr_t | |
uintptr_t | atomic_uintptr_t | |
size_t | atomic_size_t | |
ptrdiff_t | atomic_ptrdiff_t |
Functions for atomic objects (C-style)
atomic_is_lock_free | Is lock-free (function) |
atomic_init | Initialize atomic object (function) |
atomic_store | Modify contained value (function) |
atomic_store_explicit | Modify contained value (explicit memory order) (function) |
atomic_load | Read contained value (function) |
atomic_load_explicit | Read contained value (explicit memory order) (function) |
atomic_exchange | Read and modify contained value (function) |
atomic_exchange_explicit | Read and modify contained value (explicit memory order) (function) |
atomic_compare_exchange_weak | Compare and exchange contained value (weak) (function) |
atomic_compare_exchange_strong | Compare and exchange contained value (strong) (function) |
atomic_fetch_add | Add to contained value (function) |
atomic_fetch_sub | Subtract from contained value (function) |
atomic_fetch_and | Apply bitwise AND to contained value (function) |
atomic_fetch_or | Apply bitwise OR to contained value (function) |
atomic_fetch_xor | Apply bitwise XOR to contained value (function) |
Functions for atomic flags (C-style)
atomic_flag_test_and_set | Test and set atomic flag (function) |
Test and set atomic flag (explicit memory order) (function) | |
atomic_flag_clear | Clear atomic flag (function) |
atomic_flag_clear_explicit | Clear atomic flag (explicit memory order) (function) |
内存顺序(memory_order)
前面在介绍atomic相关的函数时,基本都有一个memory_order的选项可供使用者去选择。
由于CPU和编译器为了提高程序的执行效率,很有可能会对代码做乱序优化,将代码重新打乱,排序,提前缓存给cpu去执行,这个乱序优化在单线程上是没有问题的,因为编译器会保证打乱后的代码的语意和打乱之前是一样的。
但是在多线程编程中,这个乱序优化很有可能会带来副作用:因为不同线程之间的缓冲区是不可见的,如果当两个线程之间的变量有某种关系的依赖的时候,这种依赖就可能因为cpu的乱序执行而被破坏掉,因为在cpu看来,单独一个线程内,这些变量是没有依赖的,就做了乱序优化,就导致了副作用。
所以,我们使用内存顺序来控制编译器的行为,以防程序得到不预期的结果。memory_order有如下几个值可以选择:
typedef enum memory_order {
memory_order_relaxed, // relaxed
memory_order_consume, // consume
memory_order_acquire, // acquire
memory_order_release, // release
memory_order_acq_rel, // acquire/release
memory_order_seq_cst // sequentially consistent
} memory_order;
-
memory_order_relaxed
在某个时间点执行该原子操作。这是最松散的内存顺序,没有同步或顺序制约,仅对此操作要求原子性,不能保证不同线程中的内存访问是按照原子操作的顺序进行的。即只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。
-
memory_order_consume
一旦释放线程中所有对内存的访问(这些访问依赖于释放操作(并且对加载线程有副作用))都发生了,则命令执行该操作。
-
memory_order_acquire
一旦释放线程中的所有内存访问(这些访问对加载线程有副作用)都发生了,则命令执行该操作。
-
memory_order_release
该操作在消耗或获取操作之前执行,作为对内存的其他访问的同步点,这些访问可能对加载线程产生副作用。
-
memory_order_acq_rel
该操作加载获取和存储释放(如上面的memory_order_acquisition和memory_order_release所定义)。
-
memory_order_seq_cst
该操作以顺序一致的方式进行排序:使用该内存顺序的所有操作都是在所有可能对涉及的其他线程产生副作用的内存访问都已经发生之后才执行的。这是最严格的内存顺序,意味着操作不能重排,通过非原子内存访问确保线程交互之间的意外副作用最小。对于消耗和获取负载,顺序一致的存储操作被认为是释放操作。
如果没有为特定的操作指定一个顺序选项,则内存顺序选项对于所有原子类型默认都是memory_order_seq_cst,即按照最严格内存顺序执行。