C++ 多线程(automic篇)

引言     

       前面章节介绍的st::mutex可以保证多线程之间数据访问的互斥性,但是C++ 11还提供了一种原子类型,即atomic,它提供了多线程间的原子操作,它是一种不需要用到mutex技术的多线程并发编程方式,一个操作一旦开始,这个操作就不能被处理器拆分处理,能够确保所有其他线程都不在同一时间访问该资源,不会存在数据竞争的问题。

       所以对于原子操作来讲,是不可能看到原子操作只完成了部分这种情况的, 它要么是做了,要么就是没做,只有这两种可能。虽然mutex也可以提供共享资源的访问的保护,但是加锁一般针对一个代码段,atomic针对的一般都是一个变量;而且原子操作更加接近底层,因而效率一般比使用互斥量更高。可以说,原子操作轻松地化解了互斥访问共享数据的难题。

atomic原型

        该头文主要声明了两个类,std::atomic 和 std::atomic_flag,其中实现了原子类型的所有特性。另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。

  • C++ class

      std::atomic是一个模板类,模板参数是数据类型。

template 
struct atomic
  • 构造方法

既然是一个类,那肯定有对应的构造方法:

(1). 默认构造函数
       构造一个未初始化的原子对象。(后面可通过调用atomic_init进行初始化)
(2). 初始化构造函数
       由类型 T初始化一个 std::atomic对象。比如: atomic x(10);
(3). 复制构造函数[禁用]
       禁用复制构造函数,原子对象不可复制/移动。原因是原子读和原子写是两个独立的原子操作,无法保证两个独立的原子操作一起后仍然还是原子性的

生成一个T类型的原子对象,并提供了系列原子操作函数

#include  
#include  
#include  

using namespace std;

int main(void) {
    //默认构造函数
	atomic a;
	atomic_init(&a,100);

    //初始化构造函数
	atomic 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  
#include  
#include  

using namespace std;

atomic a;

int main(void) {
	atomic 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 = "<C++ 多线程(automic篇)_第1张图片

  • 特化成员函数

特化成员函数  描述 说明
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 
#include 
#include 
using namespace std;

constexpr auto THREAD_NUM = 10;

atomic 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是一种简单的原子类型,是一种bool型的原子对象,它只支持两种操作:test_and_setclear

  • 构造函数
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进行初始化,将其中的标志位置0。

  • 操作方法

      1. test_and_set()

         检测其中的标志位,如果是0,就置位1,返回0;如果是1就不变,返回1,这些操作都是原子性的。

      2. clear()

       用于把标志位置位0。使用前面说的ATOMIC_FLAG_INIT对std::atomic_flag 对象进行初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。

  • C-style atomic types

      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 ).
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_leastN_t atomic_int_leastN_t
uint_leastN_t atomic_uint_leastN_t
int_fastN_t atomic_int_fastN_t
uint_fastN_t atomic_uint_fastN_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)

atomic_flag_test_and_set_explicit

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,即按照最严格内存顺序执行。

你可能感兴趣的:(c++,开发语言)