当前位置: 首页 > 工具软件 > Atomic > 使用案例 >

C++ 多线程:std::atomic模板特化

洪伟兆
2023-12-01

cpp 多线程:std::atomic模板特化

概念

std::atomic<>的每个实例化和完全特化表示不同线程可以同时操作(它们的实例)的类型,而不会引发未定义的行为;另外,std::atomic<>通过允许各种 memory orders 为您提供更多控制权指定同步和排序约束。我前面两篇文章介绍了有关 cpp 11 原子和内存模型的更多信息,感兴趣的可以去阅读下。

对于整型和指针这些的原子类型,您可能会使用 overloaded arithmetic operatorsanother set of them 做以下简单加减操作:

std::atomic<int> value(0);
value++; //This is an atomic op
value += 5; //And so is this
 

但是,上面这些操作忽略了我们前面介绍了内存模型 memory orders 带来的性能提升。因为操作符语法不允许你指定内存顺序,这些操作只能使用 std::memory_order_seq_cst 来执行,因为这是 cpp 11 中所有原子操作的默认顺序。它保证所有原子操作之间的顺序一致性(总全局排序)。在某些情况下,这可能无法满足要求,因此您可能需要使用更明确的形式:

std::atomic<long> value {0};
value.fetch_add(2, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(2, std::memory_order_release); // Atomic, performs 'release' operation
 

而且,我们都知道计算机更擅长处理整数以及指针,并且X86架构的CPU还提供了指令级的CAS操作。cpp11为了充分发挥计算的特长,针对非浮点数值(std::atmoic<integral>)及指针(std::atomic<T\*>)进行了特化,以提高原子操作的性能。特化后的atomic在通用操作的基础上,还提供了更丰富的功能。它们拥有初等模板所不拥有的额外属性:额外支持适合指针/整型类型的原子算术运算,例如 fetch_add 、 fetch_sub等。

整数类型特化

以下整数类型之一实例化时, std::atomic 提供适合于整数类型的额外原子操作,例如 fetch_addfetch_subfetch_andfetch_orfetch_xor

  • 字符类型 char 、 char8_t (cpp20 起)、 char16_t 、 char32_t 和 wchar_t ;
  • 标准有符号整数类型:signed char 、 short 、 int 、 long 和 long long ;
  • 标准无符号整数类型:unsigned char 、 unsigned short 、 unsigned int 、 unsigned long 和 unsigned long long ;
  • 任何头文件 <cstdint> 中的 typedef 所需的额外整数类型。

类型定义

template <> struct atomic<integral> {
    atomic() = default;
    constexpr atomic(integral);
    atomic(const atomic&) = delete;
 
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
 
    void store(integral, memory_order = memory_order_seq_cst) volatile;
    void store(integral, memory_order = memory_order_seq_cst);
 
    integral load(memory_order = memory_order_seq_cst) const volatile;
    integral load(memory_order = memory_order_seq_cst) const;
 
    operator integral() const volatile;
    operator integral() const;
 
    integral exchange(integral, memory_order = memory_order_seq_cst) volatile;
    integral exchange(integral, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_weak(integral&, integral, memory_order, memory_order) volatile;
    bool compare_exchange_weak(integral&, integral, memory_order, memory_order);
 
    bool compare_exchange_strong(integral&, integral, memory_order, memory_order) volatile;
    bool compare_exchange_strong(integral&, integral, memory_order, memory_order);
 
    bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst);
    
    //原子对象的封装值加 val,并返回原子对象的旧值。整个过程是原子的
    integral fetch_add(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_add(integral, memory_order = memory_order_seq_cst);
    //原子对象的封装值减 val,并返回原子对象的旧值。整个过程是原子的
    integral fetch_sub(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_sub(integral, memory_order = memory_order_seq_cst);
    //原子地进行参数和原子对象的值的逐位与,并获得先前保有的值
    integral fetch_and(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_and(integral, memory_order = memory_order_seq_cst);
 	// 原子地进行参数和原子对象的值的逐位或,并获得先前保有的值
    integral fetch_or(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_or(integral, memory_order = memory_order_seq_cst);
 	// 原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值
    integral fetch_xor(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_xor(integral, memory_order = memory_order_seq_cst);

    atomic& operator=(const atomic&) = delete;
    atomic& operator=(const atomic&) volatile = delete;
     
    integral operator=(integral) volatile;
    integral operator=(integral);
    // 令原子值增加或减少一
    integral operator++(int) volatile; //后缀++
    integral operator++(int);
    integral operator--(int) volatile; /后缀--
    integral operator--(int);
    integral operator++() volatile; //前缀++
    integral operator++();
    integral operator--() volatile; //前缀--
    integral operator--();
    // 加、减,或与原子值进行逐位与、或、异或
    integral operator+=(integral) volatile;
    integral operator+=(integral);
    integral operator-=(integral) volatile;
    integral operator-=(integral);
    integral operator&=(integral) volatile;
    integral operator&=(integral);
    integral operator|=(integral) volatile;
    integral operator|=(integral);
    integral operator^=(integral) volatile;
    integral operator^=(integral);
};
 

指针类型特化

类型定义

template <class T> struct atomic<T*> {
    atomic() = default;
    constexpr atomic(T*);
    atomic(const atomic&) = delete;
 
    atomic& operator=(const atomic&) = delete;
    atomic& operator=(const atomic&) volatile = delete;
 
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
 
    void store(T*, memory_order = memory_order_seq_cst) volatile;
    void store(T*, memory_order = memory_order_seq_cst);
 
    T* load(memory_order = memory_order_seq_cst) const volatile;
    T* load(memory_order = memory_order_seq_cst) const;
 
    operator T*() const volatile;
    operator T*() const;
 
    T* exchange(T*, memory_order = memory_order_seq_cst) volatile;
    T* exchange(T*, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_weak(T*&, T*, memory_order, memory_order) volatile;
    bool compare_exchange_weak(T*&, T*, memory_order, memory_order);
 
    bool compare_exchange_strong(T*&, T*, memory_order, memory_order) volatile;
    bool compare_exchange_strong(T*&, T*, memory_order, memory_order);
 
    bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst);
 
    // 上面的接口我们上一篇文章已经介绍过,这里不再介绍了
    T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
    T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst);
 
    T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
    T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst);
 
    T* operator=(T*) volatile;
    T* operator=(T*);
    T* operator++(int) volatile;
    T* operator++(int);
    T* operator--(int) volatile;
    T* operator--(int);
    T* operator++() volatile;
    T* operator++();
    T* operator--() volatile;
    T* operator--();
    T* operator+=(ptrdiff_t) volatile;
    T* operator+=(ptrdiff_t);
    T* operator-=(ptrdiff_t) volatile;
    T* operator-=(ptrdiff_t);
};
 

接口说明

fetch_add

仅为 atomic<Integral> 模板特化的成员

T fetch_add (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_add (T val, memory_order sync = memory_order_seq_cst) noexcept;
 

仅为 atomic<T*> 模板特化的成员

T fetch_add (ptrdiff_t val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_add (ptrdiff_t val, memory_order sync = memory_order_seq_cst) noexcept;
 

原子地以值和 val 的算术加法结果替换当前值。运算是读修改写操作。按照 order 的值影响内存。

对于有符号 Integral 类型,定义算术为使用补码。对于 T* 类型,结果可能为未定义的地址,但此外运算无未定义行为。

#include <iostream>
#include <thread>
#include <atomic>
 
std::atomic<long long> data;
void do_work()
{
    data.fetch_add(1, std::memory_order_relaxed);
}
 
int main()
{
    std::thread th1(do_work);
    std::thread th2(do_work);
    std::thread th3(do_work);
    std::thread th4(do_work);
    std::thread th5(do_work);
 
    th1.join();
    th2.join();
    th3.join();
    th4.join();
    th5.join();
 
    std::cout << "Result:" << data << '\n';
}
 

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;
 

将原子对象的封装值减 val,并返回原子对象的旧值,适用于整形和指针类型的 std::atomic 特化版本,整个过程是原子的。其功能用伪代码表示为:

auto old = contained
contained -= v
return old
 

++, --,

自增运算符重载,有两种形式,第一种形式 (1) 返回自增后的值(即前缀++),第二种形式(2) 返回自增前的值(即后缀++),适用于整形和指针类型的 std::atomic 特化版本。

自减运算符重载,有两种形式, 第一种形式 (1) 返回自减后的值(即前缀–),第二种形式(2) 返回自减前的值(即后缀–),适用于整形和指针类型的 std::atomic 特化版本。

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

fetch_and,fetch_or,fetch_xor

位操作,将contained按指定方式进行位操作,并返回contained的旧值,仅用于整数的特化。

// T is only for integral
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
 

operator &=,operator |=,operator ^=,operator+=, operator-=

与相应的fetch_*操作不同的是,operator操作返回的是新值,仅用于整数的特化。

// T is only for integral
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;}
 
// T is for pointer
T* operator+=(ptrdiff_t) volatile;
T* operator+=(ptrdiff_t);
T* operator-=(ptrdiff_t) volatile;
T* operator-=(ptrdiff_t);
// T is for integral
integral operator+=(integral) volatile;
integral operator+=(integral);
integral operator-=(integral) volatile;
integral operator-=(integral);
 

以上各个 operator 都会有对应的 fetch_* 操作,详细见下表:简单总结一下:

操作符操作符重载函数等级的成员函数整形指针其他
+atomic::operator+=atomic::fetch_add
-atomic::operator-=atomic::fetch_sub
&atomic::operator&=atomic::fetch_and
|atomic::operator| =atomic::fetch_or
atomic::operator^=atomic::fetch_xor

std::atomic模板参数

std::atomic 模板可用任何满足可复制构造 (CopyConstructible) 及可复制赋值 (CopyAssignable) 的可平凡复制 (TriviallyCopyable) 类型 T 特化。若下列任何值为 false 则T不可用于atomic模板:

  • std::is_trivially_copyable::value
  • std::is_copy_constructible::value
  • std::is_move_constructible::value
  • std::is_copy_assignable::value
  • std::is_move_assignable::value

那么到底什么是TriviallyCopyable类型呢?有以下三种:

  • Scalar types(标量类型)

    标量类型(Scalar type)是相对复合类型(Compound type)来说的:标量类型只能有一个值,而复合类型可以包含多个值。复合类型是由标量类型构成的。

    在C语言中,整数类型(int、short、long等)、字符类型(char、wchar_t等)、枚举类型(enum)、小数类型(float、double等)、布尔类型(bool)都属于标量类型,一份标量类型的数据只能包含一个值。

    结构体(struct)、数组(array)、字符串(string)都属于复合类型,一份复合类型的数据可以包含多个标量类型的值,也可以包含其他复合类型的值。

  • rivially copyable classes(拷贝不变类),一个类型如果是trivially copyable,则使用memcpy这种方式把它的数据从一个地方拷贝出来会得到相同的结果。编译器如何判断一个类型是否trivially copyable呢?cpp标准把trivial类型定义如下,一个拷贝不变(trivially copyable)类型是指::

    • At least one copy constructor, move constructor, copy assignment operator, or move assignment operator is eligible,必须有一个拷贝构造函数,或者移动构造函数,或者拷贝赋值运算符,或者移动赋值运算符;
    • Every eligible copy constructor (if any) is trivial,每个复制构造函数均为不变的;
    • Every eligible move constructor (if any) is trivial,每个移动构造函数均为不变的;
    • Every eligible copy assignment operator (if any) is trivial,每个复制赋值运算符均为不变的;
    • Every eligible move assignment operator (if any) is trivial,每个移动赋值运算符均为不变的;
    • Has a trivial non-deleted destructor,平凡而未弃置的析构函数;
  • Arrays of TriviallyCopyable objects可平凡复制 (TriviallyCopyable) 对象的数组。

一个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是trivial,它的array也是trivial
    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的,否则不是。

参考

std::atomic - cpp中文 - API参考文档 (apiref.com)

 类似资料: