5.2.1 标准原子类型
标准原子类型(atomic types)可以在头文件中找到。所有在这种类型上的操作都是原子的,虽然可以使用互斥量去达到原子操作的效果,但只有在这些类型上的操作是原子的(语言明确定义)。实际上,标准原子类型都很相似:它们(大多数)都有一个is_lock_free()成员函数,这个函数允许用户决定是否直接对一个给定类型使用原子指令(x.is_lock_free()返回true),或对编译器和运行库使用内部锁(x.is_lock_free()返回false)。
只用std::atomic_flag类型不提供is_lock_free()成员函数。这个类型是一个简单的布尔标 志,并且在这种类型上的操作都需要是无锁的(lock-free);当你有一个简单无锁的布尔标志 是,你可以使用其实现一个简单的锁,并且实现其他基础的原子类型。当你觉得“真的很简 单”时,就说明:在 std::atomic_flag 对象明确初始化后,做查询和设置(使用test_and_set() 成员函数),或清除(使用clear()成员函数)都很容易。这就是:无赋值,无拷贝,没有测试和清 除,没有其他任何操作。
剩下的原子类型都可以通过特化std::atomic<>类型模板而访问到,并且拥有更多的功能,但可能不都是无锁的(如之前解释的那样)。在最流行的平台上,期望原子变量都是无锁的内置类型(例如 std::atomic<int>和std::atomic<void*>),但这没有必要。你在后面将会看到,每个特化接口所反映出的类型特点;位操作(如&=)就没有为普通指针所定义,所以它也就不能为原 子指针所定义。
除了直接使用std::atomic<>类型模板外,你可以使用在表5.1中所示的原子类型集。由于历 史原因,原子类型已经添加入C++标准中,这些备选类型名可能参考相应的std::atomic<>特 化类型,或是特化的基类。在同一程序中混合使用备选名与std::atomic<>特化类名,会使代码的移植大打折扣。
表5.1 标准原子类型的备选名和与其相关的 std::atomic<> 特化类
原子类型 | 相关特化类 |
atomic_bool | std::atomic<bool> |
atomic_char | std::atomic<char> |
atomic_schar | std::atomic<signed char> |
atomic_uchar | std::atomic<unsigned char> |
atomic_int | std::atomic<int> |
atomic_uint | std::atomic<unsigned> |
atomic_short | std::atomic<short> |
atomic_ushort | std::atomic<unsigned short> |
atomic_long | std::atomic<long> |
atomic_ulong | std::atomic<unsigned long> |
atomic_llong | std::atomic<long long> |
atomic_ullong | std::atomic<unsigned long long> |
atomic_char16_t | std::atomic<char16_t> |
atomic_char32_t | std::atomic<char32_t> |
atomic_wchar_t | std::atomic<wchar_t> |
C++标准库不仅提供基本原子类型,还定义了与原子类型对应的非原子类型,就如同标准库中的 std::size_t 。如表5.2所示这些类型:
表5.2 标准原子类型定义(typedefs)和对应的内置类型定义(typedefs)
原子类型定义 | 标准库中相关类型定义 |
atomic_int_least8_t | int_least8_t |
atomic_uint_least8_t | uint_least8_t |
atomic_int_least16_t | int_least16_t |
atomic_uint_least16_t | uint_least16_t |
atomic_int_least32_t | int_least32_t |
atomic_uint_least32_t | uint_least32_t |
atomic_int_least64_t | int_least64_t |
atomic_uint_least64_t | uint_least64_t |
atomic_int_fast8_t | int_fast8_t |
atomic_uint_fast8_t | uint_fast8_t |
atomic_int_fast16_t | int_fast16_t |
atomic_uint_fast16_t | uint_fast16_t |
atomic_int_fast32_t | int_fast32_t |
atomic_uint_fast32_t | uint_fast32_t |
atomic_int_fast64_t | int_fast64_t |
好多种类型!不过,它们有一个相当简单的模式;对于标准类型进行typedef T,相关的原子类型就在原来的类型名前加上atomic_的前缀:atomic_T。除了singed类型的缩写是s, unsigned的缩写是u,和long long的缩写是llong之外,这种方式也同样适用于内置类型。对 于 std::atomic<T> 模板,使用对应的T类型去特化模板的方式,要好于使用别名的方式。
通常,标准原子类型是不能拷贝和赋值,他们没有拷贝构造函数和拷贝赋值操作。但是,因为可以隐式转化成对应的内置类型,所以这些类型依旧支持赋值,可以使用load()和store()成员函数,exchange()、compare_exchange_weak()和compare_exchange_strong()。它们都支持复合赋值符:+=, -=, *=, |= 等等。并且使用整型和指针的特化类型还支持 ++ 和 --。当 然,这些操作也有功能相同的成员函数所对应:fetch_add(), fetch_or() 等等。返回值通过赋值操作返回,并且成员函数不是对值进行存储(在有赋值符操作的情况下),就是对值进行操作(在命名函数中)。这就能避免赋值操作符返回引用。为了获取存储在引用的的值,代码需要执行单独的读操作,从而允许另一个线程在赋值和读取进行的同时修改这个值,这也就为条件竞争打开了大门。
std::atomic<> 类模板不仅仅是一套特化的类型,其作为一个原发模板也可以使用用户定义类型创建对应的原子变量。因为,它是一个通用类模板,很多成员函数的操作在这种情况下有所限制:load(), store()(赋值和转换为用户类型), exchange(), compare_exchange_weak()和 compare_exchange_strong()。
每种函数类型的操作都有一个可选内存排序参数,这个参数可以用来指定所需存储的顺序。 在5.3节中,会对存储顺序选项进行详述。现在,只需要知道操作分为三类:
1)Store操作,可选如下顺序:memory_order_relaxed, memory_order_release, memory_order_seq_cst。
2)Load操作,可选如下顺序:memory_order_relaxed, memory_order_consume,memory_order_acquire, memory_order_seq_cst。
3)Read-modify-write(读-改-写)操作,可选如下顺序:memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst。 所有操作的默认顺序都是memory_order_seq_cst。
现在,让我们来看一下每个标准原子类型进行的操作,就从 std::atomic_flag 开始吧。