如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:
即使是简单的语句,C++ 也不保证是原子操作。
CPU 可能会调整指令的执行顺序。
在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。
利用 C++ 的 atomic<T> 能完成对象的原子的读、写以及RMW(read-modify-write),而参数 std::memory_order 规定了如何围绕原子对象的操作进行排序。memory order 内存操作顺序其实是 内存一致性模型 (Memory Consistency Model),解决处理器的 write 操作什么时候能够影响到其他处理器,或者说解决其他处理处理器什么时候能够观测到当且 写CPU/写线程 写入内存的值,有了 memory odering,我们就能知道其他处理器是怎么观测到 store 指令的影响的。
一致模型有很多种,在 Wikipedia 里面搜索 Consistency model 即可看到,目前 C++ 所用到有 Sequential Consistency 和 Relaxed Consistency 以及 Release consistency。
Memory Operation Ordering
我们所编写的程序会定义一系列的 load 和 store 操作,也就是 Program ordering,这些 load 和 store 的操作应用在内存上就有了内存操作序(memory operation ordering),一共有四种内存操作顺序的限制,不同的内存一致模型需要保持不同级别的操作限制,其中 W 代表写,R 代表读:
W -> R:写入内存地址 X 的操作必须比在后面的程序定义序列的读取地址 Y 之前提交 (commit), 以至于当读取内存地址 Y 的时候,写入地址 X 的影响已经能够在读取Y时被观测到。
R -> R: 读取内存地址 X 的操作必须在后序序列中的读取内存地址 Y 的操作之前提交。
R -> W:读取内存地址 X 的操作必须在后序序列中读取内存地址 Y 的操作之前提交。
W -> W:写入内存地址 X 的操作必须在后续序列中写入内存地址 Y 的操作之前提交。
提交的意思可以理解为,后面的操作需要等前面的操作完全执行完才能进行下一个操作。
the result of any execution is the same as-if (任何一种执行结果都是相同的就好像)
the operations of all threads are executed in some sequential order (所有线程的操作都在某种次序下执行)
the operations of each thread appear in this sequence in the order specified by their program (在全局序列中的,各个线程内的操作顺序由程序指定的一致)
组合起来:全局序列中的操作序列要和线程所指定的操作顺序要对应,最终的结果是所有线程指定顺序操作的排列,不能出现和程序指定顺序组合不出来的结果。
怎么做会违反 sequcential consistency(SC)?也就是 SC 的反例是什么?
乱序执行 (out-of-order)
内存访问重叠,写A的过程中读取A,宽于计算机word的,64位机器写128位变量
更加形象的理解可以从内存的角度来看:
所有的处理器都按照 program order 发射 load 和 store 的操作,而内存一个地一个地从上面 4 个处理器中读取指令,并且仅当完成一个操作后才会去执行下一个操作,类似于多个 producer 一个 consumer 的情况。
Acquire:如果一个操作 X 带有 acquire 语义,那么在操作 X 后的所有 load/store 指令都不会被重排序到操作 X 之前,其他处理器会在看到操作X后序操作的影响之前看到操作 X 的影响,也就是必须先看到 X 的影响,再是后续操作的影响。
Relase:如果一个操作 X 带有 release 语义,那么在操作 X 之前的所有 load/store 指令操作都不会被重排序到操作 X 之后,其他处理器会先看到操作 X 之前的操作。
Acquire/Release 常用在互斥锁(mutex lock)和自旋锁(spin lock),获得一个锁和释放一个锁需要分别使用 Acquire 和 Release 语义防止指令操作被重排出临界区,从而造成数据竞争。
Acquire/Consume
Acquire/Consume 对应 std::memory_order_acquire 和 std::memory_order_consume,两种内存模型的组合仅有 consume 不同于 release,不同点在于,假设原子操作 X, Release 会防止 X 之前的所有指令不会被重排到 X 之后,而 Consume 只能保证依赖的变量不会被重排到 X 之后,引入了依赖关系。