前言
原子类型操作(atomic operation)具备不可分割性,其仅存在执行完成与未执行两种状态。
如若某线程正在以原子操作load一个对象的值(对该对象的所有修改都由原子操作完成),那么它仅能读取到该对象在修改前,亦或某一次修改后的值(无法获取中间状态)。非原子类型操作不具备此性质,因此线程很可能读取到某个对象的中间状态,从而造成数据竞争。
标准原子类型
所有标准原子类型都可以在<atomic>
头文件中找到。尽管从理论角度出发,开发者可以使用互斥锁保证任何数据类型均对外展现出原子性,但从C++语言定义而言,仅有这些类型是真正的原子类型。
事实上,可能标准原子类型也是通过使用锁实现了原子性。为了探究这一点,几乎所有标准原子类型(特例为std::atomic_flag
)都具备这样一个接口is_lock_free()
,开发者可以通过调用此接口来判断,当前行为究竟是语义上的原子行为(此时该接口返回true)还是编译器或者函数库内部在执行加锁操作模拟原子性(此时该接口返回false)。了解原子类型是否具备语义上的原子性在性能优化领域较为重要:如果某类型内部同样使用了mutex,那么可能将无法带来期望的性能收益。
除了直接使用std::atomic
类模板外,标准库也提供了一系列名称来引用原子类型。但由于历史原因,在较老的编译器上,这些替代类型名称可能引用对应 std::atomic<>的特化类或这个特化类的一个基类,支持C++17的编译器则不存在此问题。因此,如果混合使用类型名与std::atomic<>
特化类,可能导致代码不可移植。
类型名与std::atomic<>
特化类映射表如下:
除此以外,C++标准库还针对std::size_t
等非基本原子类型提供typedef,其映射表如下:
标准原子类型理论上不可复制或赋值,因为它们没有copy ctor
或copy assignment operator
。但它们支持通过诸如load()
,store
,exchange
等被相应的内置类型赋值或隐式转换。此外,它们也支持相应的相关计算符,例如+=
,-=
等等。整形与指针的原子类型还支持++
,--
。这些计算符同样具备实现同样功能的成员函数,如fetch_add()
、fetch_or()
等等。
一般而言,对象的赋值运算符往往返回引用,如果将该惯例应用于原子类型,则获取存储数据时程序中势必存在一个特殊的读操作,这可能导致数据竞争(在assign和read之间可能存在一个线程正在修改数据)。因此,对于赋值运算符而言,其返回值是被存储的数据,而对于命名成员函数而言,返回值是操作前的数据。
std::atomic<>
类模板不仅仅是一组特化。它确实存在一个主模板,可用于创建用户自定义类型的原子变体。其操作被限定为load()
、store()
、exchange()
、compare_exchange_weak()
和compare_exchange_strong()
。
每个原子类型上操作都有一个可选的内存顺序参数,它是std::memory_order
枚举类型的某个值,可以用来指定所需的内存顺序语义。std::memory_order
枚举值分别为:
1.std::memory_order_relaxed
2.std::memory_order_acquire
3.std::memory_order_consume
4.std::memory_order_acq_rel
5.std::memory_order_release
6.std::memory_order_seq_cst
该内存顺序参数依赖于操作的类别,在不指定的情况下,默认使用std::memory_order_seq_cst
。
std::atomic_flag
std::atomic_flag
是最简单的标准原子类型,表征布尔量,此类型的对象仅有两种状态:set或clear。实际上,它被设计为一种基础构建模块,很少应用于实际工程。以下将简称std::atomic_flag
类型对象为flag对象。
flag对象必须用ATOMIC_FLAG_INIT
初始化,即初始化为clear状态,这也意味着它无法被初始化为set状态。1
std::atomic_flag f = ATOMIC_FLAG_INIT;
它是唯一在初始化时需要这种特殊处理的原子类型,也是唯一保证无锁的原子类型。此外,static flag对象保证其具备静态初始化特性,即该对象总在首次使用前完成初始化。
flag对象在完成初始化后,仅能对其施加三种操作:destroy,clear,test_and_set(读取值后set),分别对应着析构函数,clear()
成员函数以及test_and_set()
成员函数。clear()
和test_and_set()
都可以指定内存顺序。
所有原子类型均不支持copy ctor && copy assignment,flag对象也不例外。原因很简单:应用于原子类型的所有操作均应具备原子性,但赋值和拷贝构造涉及到两个独立对象。在执行拷贝构造或拷贝赋值操作时,必须先从一个对象读取值,然后将其写入另一个对象,这一组合行为不可能具备原子性。
std::atomic_flag
所具备的特性使其非常适合实现自旋互斥锁,具体实现如下所示:1
2
3
4
5
6
7
8
9
10
11class spinlock_mutex {
std::atomic_flag flag;
public:
spinlock_mutex(): flag(ATOMIC_FLAG_INIT) {}
void lock() {
while(flag.test_and_set(std::memory_order_acquire)); // 忙-等待
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
flag对象类似于bool,但其未提供标准get接口(C++20引入test()
成员函数),因此如存在相关需求,请使用std::atomic<bool>
。
std::atomic<bool>
相较于flag对象,std::atomic<bool>
的灵活程度大为提高。尽管不支持copy ctor与copy assign,但可以使用内置的bool类型构造它——这意味着它可以被初始化为true或false。此外,std::atomic<bool>
对象亦可接受来自内置bool类型的赋值。
1 | std::atomic<bool> b(true); |
这里需要注意一点:不同于常规赋值操作,原子类型的赋值操作返回的并非引用,而是被赋予的值。原因很简单,返回引用可能会导致另一个线程修改当前存储数据,这将导致数据竞争。
std::atomic<bool>
使用strore()
成员函数写入true或false值,使用exchange()
成员函数完成对原始值的检索与替换,读取操作使用的是成员函数load()
。1
2
3
4std::atomic<bool> b;
bool x=b.load(std::memory_order_acquire);
b.store(true);
x=b.exchange(false, std::memory_order_acq_rel);
除exchange
外,std::atomic<bool>
还有两个”read-change-write”操作,分别是compare_exchange_weak()
和compare_exchange_strong()
。
compare-exchange操作是使用原子类型进行编程的基石——它将原子变量的值与提供的预期值进行比较,若相等,则存储提供的值。如果值不相等,则预期值将被更新为原子变量存储的值。该操作返回布尔量,若执行了存储操作(值相等)且存储成功则为true,否则为false。
compare_exchange_weak()
的weak之处在于,即使原始值与预期值一致时,存储也可能会不成功,此时原始值不变,compare_exchange_weak()
返回false。这种情况多见于当前机器不支持compare-exchange指令(意味着无法保证操作原子性),若此时执行操作的线程在必要的指令序列中被切换出去,而操作系统在其位置调度了另一个线程,将导致数据存储失败。因此compare_exchange_weak()
通常应用于循环内:1
2
3
4
5
6bool expected=false;
extern atomic<bool> b; // 在其他某个地方设置
// b内部数据为存储值
// expected为期待值
// true为提供值
while(!b.compare_exchange_weak(expected,true) && !expected); // 若存储失败,将持续执行compare_exchange_strong()
的strong之处在于,当存储值等于期待值时,该函数返回ture(即使存储失败),否则返回false。
若当前提供值依赖于存储值,则开发者可以利用期待值被更新为存储值的特性,利用期待值计算提供值。这里需要注意,若这种计算过程非常耗时,则使用strong优于weak,因为weak所在的循环将导致提供值被反复计算,即使期待值未发生变更。
除flag外,原子类型并不保证无锁性,若有需要,std::atomic<bool>
可以通过is_lock_free()
成员函数检查其操作是否具备无锁性。
std::atomic<T*>
std::atomic<T*>具备所有std::atomic
std::atomic
除上述接口外,std::atomicfetch_add()
与fetch_sub()
,其返回值与上述接口不同,返回执行加/减操作前的原始值。
std::atomic1
2
3
4
5
6
7
8
9class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo*x=p.fetch_add(2); //p加2,并返回老的值
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1); //p减1,并返回新的值
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);
标准整形原子类型
除load(), store(), exchange(), compare_exchange_weak(),compare_exchange_strong()接口外,原子整形类型如std::atomic
类似于指针操作,fetch()
接口均返回修改前的旧值,前置操作符返回修改后的新值,后置操作符返回修改前的旧值。
std::atomic<T>
给定用户自定义的类型UDT,std::atomic<UDT>提供了与std::atomic<bool>相同的接口,当然,与存储值相关的返回类型为UDT,而非bool。
并非任意UDT均可与std::atomic<>搭配使用:UDT必须具备一个trivial拷贝赋值操作符,这意味着该类型必须没有虚函数和虚基类,并且必须使用编译器生成的拷贝赋值操作符——这一要求对其基类和非静态数据成员同样生效。这保证了编译器可以使用memcpy(或一个等价操作)来完成赋值。此外,compare_exchange使用的是memcmp式的比较操作,因此不支持bitwise相等的UDT实例在执行compare_exchange操作时将会不符合预期,即使比较值满足自定义operator==(顺带提一嘴,std::atomic<double>与std::atomic<float>尽管是标准类型,使用compare_exchange_strong
时也存在同样的坑,原因在于数据的存储形式不同)。
这些限制背后的原因前文已经提及——不要将指针和引用作为参数传递给用户提供的函数,从而在锁范围外传递受保护数据。一般来说,编译器无法为std::atomic<UDT生成无锁代码,因此它必须生成和使用一个内部锁。如果允许用户提供自定义拷贝赋值或比较操作符,则需要将对受保护数据的引用作为参数传递给用户提供的函数,这可能导致数据竞争。此外,这些限制增加了编译器直接为 std::atomic<UDT>使用原子指令的机会,因为它可以直接将UDT实例视为一组原始字节。
这些限制意味着开发者不能创建一些较为复杂的原子数据类型,例如std::atomic<std::vector<int>>
(它有一个特殊的拷贝构造和拷贝赋值操作符)。但这并不算大问题——数据结构越复杂,对其进行复杂操作的可能性越高,而不只是原子操作里简单的赋值和比较,此类数据最好使用std::mutex
,以确保为所需操作适当地保护数据。
如果UDT的大小 <= int或void*,那么大多数平台都能够对std::atomic<UDT>使用原子指令。一些平台还能对两倍于int或void*大小的用户自定义类型使用原子指令。这些平台通常支持所谓的双字比较-交换(double-word-compare-and-swap)指令,该指令对应于compare_exchange_xxx函数。
综上,下表展示了std::atomic<T>上可使用的各项操作:
应用于原子操作的自由函数
TODO::不甚重要,日后补充