C++11多线程(std::atomic)

在化学中原子不是可分割的最小单位,引申到编程中,原子操作是不可打断的最低粒度操作,是线程安全的。C++11中原子类提供的成员函数都是原子的,是线程安全的。

原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。

1. std::atomic_flag

C++11中所有的原子类都是不允许拷贝、不允许Move的,atomic_flag也不例外。atomic_flag顾名思议,提供了标志的管理,标志有三种状态:clear、set和未初始化状态。

1.1 atomic_flag实例化

缺省情况下atomic_flag处于未初始化状态。除非初始化时使用了ATOMIC_FLAG_INIT宏,则此时atomic_flag处于clear状态。

1.2 std::atomic_flag::clear

调用该函数将会把atomic_flag置为clear状态。clear状态您可以理解为bool类型的false,set状态可理解为true状态。clear函数没有任何返回值:

void clear(memory_order m = memory_order_seq_cst) volatile noexcept;
void clear(memory_order m = memory_order_seq_cst) noexcept;

对于memory_order我们会在后面的章节中详细介绍它,现在先列出其取值及简单释义

序号意义
1 memory_order_relaxed 宽松模型,不对执行顺序做保证
2 memory_order_consume 当前线程中,满足happens-before原则。
当前线程中该原子的所有后续操作,必须在本条操作完成之后执行
3 memory_order_acquire 当前线程中,操作满足happens-before原则。
所有后续的操作必须在本操作完成后执行
4 memory_order_release 当前线程中,操作满足happens-before原则。
所有后续的操作必须在本操作完成后执行
5 memory_order_acq_rel 当前线程中,同时满足memory_order_acquire和memory_order_release
6 memory_order_seq_cst 最强约束。全部读写都按顺序执行

1.3 test_and_set

该函数会检测flag是否处于set状态,如果不是,则将其设置为set状态,并返回false;否则返回true。

test_and_set是典型的read-modify-write(RMW)模型,保证多线程环境下只被设置一次。下面代码通过10个线程,模拟了一个计数程序,第一个完成计数的会打印"win"。
#include <atomic>
#include <iostream>
#include <list>
#include <thread>

void race(std::atomic_flag &af, int id, int n)
{
for (int i = 0; i < n; i++);
// 第一个完成计数的打印:Win if (!af.test_and_set())
  { printf(
"%s[%d] win!!! ", __FUNCTION__, id);   } } int main()
{ std::atomic_flag af
= ATOMIC_FLAG_INIT; std::list<std::thread> lstThread; for (int i = 0; i < 10; i++)
  { lstThread.emplace_back(race, std::
ref(af), i + 1, 5000 * 10000);   } for (std::thread &thr : lstThread)
  { thr.join(); }
return 0; }

程序输出如下(每次运行,可能率先完成的thread不同):

race[7] win!!!
 

2. std::atomic<T>

std::atomic是一个模板类,它定义了一些atomic应该具有的通用操作,我们一起来看一下:

2.1 is_lock_free

bool is_lock_free() const noexcept;
bool is_lock_free() const volatile noexcept;
atomic是否无锁操作。如果是,则在多个线程访问该对象时不会导致线程阻塞(可能使用某种事务内存transactional memory方法实现lock-free的特性)。
事实上该函数可以做为一个静态函数。所有指定相同类型T的atomic实例的is_lock_free函数都会返回相同值。

2.2 store

void store(T desr, memory_order m = memory_order_seq_cst) noexcept;
void store(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T operator=(T d) noexcept;
T operator=(T d) volatile noexcept;

赋值操作。operator=实际上内部调用了store,并返回d。

T operator=(T d) volatile noexpect {
    store(d);
    return d;
}

注:有些编译器,在实现store时限定m只能取以下三个值:memory_order_consume,memory_order_acquire,memory_order_acq_rel。

2.3 load

T load(memory_order m = memory_order_seq_cst) const volatile noexcept;
T load(memory_order m = memory_order_seq_cst) const noexcept;
operator T() const volatile noexcept;
operator T() const noexcept;

读取,加载并返回变量的值。operator T是load的简化版,内部调用的是load(memory_order_seq_cst)形式。

2.4 exchange

T exchange(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T exchange(T desr, memory_order m = memory_order_seq_cst) noexcept;

交换,赋值后返回变量赋值前的值。exchange也称为read-modify-write操作。

2.5 compare_exchange_weak

bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) noexcept;

这就是有名的CAS(Compare And Swap: 比较并交换)。但C++11针对该操作提供了更多的细节,其操作流程如下:

以上只是个示意图,compare_exchange_weak操作是原子的,排它的。其它线程如果想要读取或修改该原子对象时,会等待先该操作完成。
该函数直接比较原子对象所封装的值与expect的物理内容,在某些情况下,对象的比较操作在使用 operator==() 判断时相等,但 compare_exchange_weak 判断时却可能失败,因为对象底层的物理内容中可能存在位对齐或其他逻辑表示相同但是物理表示不同的值(比如 true 和 5,它们在逻辑上都表示"真",但在物理上两者的表示并不相同)。
与strong版本不同,weak版允许返回伪false,即使原子对象所封装的值与expect的物理内容相同,也仍然返回false。但它在某些平台下会取得更好的性能,在某些循环算法中这种行为也是可接受的。对于非循环算法建议使用compare_exchange_strong。

2.6 compare_exchange_strong

bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong(T& expc, T desr, memory_order m = memory_order_seq_cst) noexcept;

compare_exchange的strong版本,进行compare时,与weak版一样,都是比较的物理内容。与weak版不同的是,strong版本不会返回伪false。即:原子对象所封装的值如果与expect在物理内容上相同,strong版本一定会返回true。其所付出的代价是:在某些需要循环检测的算法,或某些平台下,其性能较compare_exchange_weak要差。但对于某些不需要采用循环检测的算法而言, 通常采用compare_exchange_strong 更好。

3. std::atomic特化

我知道计算擅长处理整数以及指针,并且X86架构的CPU还提供了指令级的CAS操作。C++11为了充分发挥计算的特长,针对非浮点数值(std::atmoic<integral>)及指针(std::atomic<T*>)进行了特化,以提高原子操作的性能。特化后的atomic在通用操作的基础上,还提供了更丰富的功能。

3.1 fetch_add

// T is integral
T fetch_add(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

该函数将原子对象封装的值加上v,同时返回原子对象的旧值。其功能用伪代码表示为:

auto old = contained
contained += v
return old

其中contained为原子对象封装值,本文后面均使用contained代表该值。注: 以上是为了便于理解的伪代码,实际实现是原子的不可拆分的。

3.2 fetch_sub

// T is integral
T fetch_sub(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

该函数将原子对象封装的值减去v,同时返回原子对象的旧值。其功能用伪代码表示为:

auto old = contained
contained -= v
return old

3.3 ++, --, +=, -=

不管是基于整数的特化,还是指针特化,atomic均支持这四种操作。其用法与未封装时一样,此处就不一一列举其函数原型了。

4. 独属于数值型特化的原子操作 - 位操作

4.1 fetch_and,fetch_or,fetch_xor

位操作,将contained按指定方式进行位操作,并返回contained的旧值。

integral fetch_and(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_and(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) noexcept;

以xor为例,其操作相当于

auto old = contained
contained ^= v
return old

4.2 operator &=,operator |=,operator ^=

与相应的fetch_*操作不同的是,operator操作返回的是新值:

T operator &=(T v) volatile noexcept {return fetch_and(v) & v;}
T operator &=(T v) noexcept {return fetch_and(v) & v;}
T operator |=(T v) volatile noexcept {return fetch_or(v) | v;}
T operator |=(T v) noexcept {return fetch_or(v) | v;}
T operator ^=(T v) volatile noexcept {return fetch_xor(v) ^ v;}
T operator ^=(T v) noexcept {return fetch_xor(v) ^ v;}

5. std::atomic的限制:trivially copyable

上面我们提到std::atomic提供了通用操作,其实这些操作可以应用到所有trivially copyable的类型。trivially copyable在cppreference中文站被译为“可平凡复制”。网上也有人译作拷贝不变。一个类型如果是trivially copyable,则使用memcpy这种方式把它的数据从一个地方拷贝出来会得到相同的结果。因此本文使用拷贝不变这个中文翻译,请大家不要纠结中文翻译,明白本文所表达的意思即可。编译器如何判断一个类型是否trivially copyable呢?C++标准把trivial类型定义如下,一个拷贝不变(trivially copyable)类型是指:

  1. 没有non-trivial 的拷贝构造函数
  2. 没有non-trivial的move构造函数
  3. 没有non-trivial的赋值操作符
  4. 没有non-trivial的move赋值操作符
  5. 有一个trivial的析构函数

一个trivial class类型是指有一个trivial类型的默认构造函数,而且是拷贝不变的(trivially copyable)的class。特别注意,拷贝不变类型和trivial类型都不能有虚机制。那么trivial和non-trivial类型到底是什么呢?这里给出一个非官方、不严谨的判断方式,方便大家对trivially copyable有一个直观的认识。一个trivial copyable类在四个点上没有自定义动作,也没有编译器加入的额外动作(如虚指针初始化就属额外动作),这四个点是:

  • 缺省构造。类必须支持缺省构造,同时类的非静态成员也不能有自定义或编译器加入的额外动作,否则编译器势必会隐式插入额外动作来初始化非静态成员。
  • 拷贝构造、拷贝赋值
  • move构造、move赋值
  • 析构

为了加深理解,我们来看一下下面的例子(所有的类都是trivial的):

// 空类
struct A1 {};

// 成员变量是trivial的
struct A2 {
    int x;
};

// 基类是trivial的
struct A3 : A2 {
    // 非用户自定义的构造函数(使用编译器提供的default构造)
    A3() = default;
    int y;
};

struct A4 {
    int a;
private: // 对防问限定符没有要求,A4仍然是trivial的
    int b;
};

struct A5 {
    A1 a;
    A2 b;
    A3 c;
    A4 d;
};

struct A6 {
    A2 a[16];
};

struct A7 {
    A6 c;
    void f(); // 普通成员函数是允许的
};

struct A8 {
     int x;
    // 对静态成员无要求(std::string是non-trivial的)
     static std::string y;
};

struct A9 {
    // 非用户自定义
    A9() = default;
    // 普通构造函数是可以的(前提是我们已经有了非定义的缺省构造函数)
    A9(int x) : x(x) {};
    int x;
};

而下面这些类型都是non-trivial的

struct B {
    // 有虚函数(编译器会隐式生成缺省构造,同时会初始化虚函数指针)
    virtual f();
};

struct B2 {
    // 用户自定义缺省构造函数
    B2() : z(42) {}
    int z;
};

struct B3 {
    B3();
    int w;
};
// 虽然使用了default,但在缺省构造声明处未指定,因此被判断为non-trivial的
NonTrivial3::NonTrivial3() = default;

struct B4 {
   // 虚析构是non-trivial的
    virtual ~B4();
};

STL在其头文件<type_traits>中定义了对trivially copyable类型的检测:

template <typename T>
struct std::is_trivially_copyable;
判断类A是否trivially copyable:std::is_trivially_copyable<A>::value,该值是一个const bool类型,如果为true则是trivially copyable的,否则不是。


转载于:C++11多线程-原子操作(1) - 简书 (jianshu.com)
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/tingtaishou/p/14994624.html