Atomic
C++11引入了一堆新玩具, 其中STL封装了一个叫atomic的东西std::atomic
.
std::atomic
定义了唯一一种不可能产生数据争用的数据类型, 任何线程写入原子变量的行为都是明确定义的.
本篇我们来聊一聊这个东西的用法和实现细节.
WHAT
atomic类型需要引入atomic头文件, 头文件具体位置一般在/usr/include/c++/x.x.x/atomic
.
#include <atomic>
atomic头文件打开看一下, 它是这么定义的:
template<typename _Tp>
struct atomic;
atomic是一种模板类型, 也就意味Atomic支持以下的写法:
std::atomic<T> variable;
目前(至C++20)atomic支持bool, 所有整数类型(ptr, char, int, uint16_t, uint32_t, uint64_t…), 浮点类型(float, double, C++20后加入), 部分智能指针(shared_ptr, weak_ptr, C++20), 或者自定义的trivially-copyable类型也可以, 具体的看cpprefrence.
你可以用以下的方法判断你的atomic是不是"ill-formed":
std::is_trivially_copyable<T>::value
std::is_copy_constructible<T>::value
std::is_move_constructible<T>::value
std::is_copy_assignable<T>::value
std::is_move_assignable<T>::value
任何一个false就表示你这个atomic有问题.
接下来就像一个普通变量一样用就好了.
一个atomic变量有一些原子操作, 来康康:
fetch_add
fetch_sub
fetch_and
fetch_or
fetch_xor
operator++
operator--
operator+=
operator-=
operator&=
operator|=
operator^=
operator=
store
load
这些操作都保证原子性, 其他的操作就不行, 比如说a = a + 1
, 这个操作语义上和a++
一样, 但是编译器不会认为这是个原子操作, 它将导致a.load()(原子), 然后在该值与最终结果a.store()(也是原子). std::memory_order_seq_cst将在这里使用.
std::mem_order是内存顺序, 有6种:
memory_order_relaxed
memory_order_consume
memory_order_acquire
memory_order_release
memory_order_acq_rel
memory_order_seq_cst
这些mem order我也没有完全弄懂(其实也没必要提, 一般用不上), 不过可以说一下第一个内存顺序, 因为确实可能会发生问题, 比如以下来自refrence的代码:
// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
可以看到relaxed下可能变成x=y=42
. 确实, 这里每个操作都是原子的, 但是顺序呢就有点问题, 你永远不知道是x=y=42
还是x=y,y=42
, 反正就是不要这么写.
HOW
来康康这个东西是怎么实现的. 我们首先引入一个概念–Cache, Cache都知道, 但是Cache这个东西是怎么和atomic关联起来的.
首先我们要明白, 真正意义上的"无锁"是很难做到的, 很多所谓的无锁只是把锁藏到程序员看不到的地方, 比如说CPU内部.
先说个和Cache没关系的实现方式, 总线锁(Bus Lock).
总线锁在原子操作执行的时候会直接锁住总线, 阻止其他线程执行内存操作, 这不就一致了. 但是问题来了, 别的线程又不要修改这个变量, 那锁了内存岂不是很蠢.
接下来引出我们的带明星缓存锁(Cacheline Lock).
缓存锁仅锁住在高速缓存中的变量, 其他的地址访问依然可以照常. 当CPU-1尝试对一个地址原子递增, 它会将cacheline标记为locked, 并设置为Exclusive, 直到执行完成操作, 然后将cacheline设置为unlock. 当有其他的CPU, 比如CPU-2也想执行一波原子递增, 它会向CPU-1发送Invalidate消息, 然后等CPU-1发送回ACK Invalidate再执行原子操作.
当然具体实现没有我这里说的那么简单, 但是也大差不差, 有兴趣的话可以了解下一个叫MESI协议(又称Illinois protocol)的协议, 这个协议用来保证多核缓存一致性.
对于某些硬件架构也有不同的实现, 比如aarch64就不是上述任何一种实现. 也有软实现的, 比如CAS(compare and swap).
相关资料