1 原子性
1.1 CPU缓存
L1、L2:一级缓存、二级缓存,均为核心独有
L3:三级缓存,多个核心共用
多级缓存,弥补CPU与内存速度不匹配的问题
1.2 cache line
缓存进行管理的一个最小存储单元,缓存块
1.3 CPU读写数据
写直达策略
缓存中任何一个字节的修改,都会立刻传播到内存
效率很低
写回策略
写操作:
- 首先判断是否命中缓存,若命中,直接写在缓存中,并标记为脏数据
- 若没有命中缓存,则通过LRU算法定位一块新的缓存块,若其为脏数据,则刷到主存,并写入新的数据,标记为脏;若不为脏数据,则直接写入新的数据,标记为脏
读操作:
- 首先判断是否命中缓存,命中直接返回
- 若没有命中缓存,则通过LRU算法定位一块新的缓存块,若其为脏数据,则刷到主存,并写入新的数据,标记为非脏;若不为脏数据,则直接写入新的数据,标记为非脏
对于多个核心的情况下,写回策略存在问题:
假如一个处理器有两个核心,分别是A、B,A中写入i=10,此时内存中的数据为i=5,此时B读取数据,B直接从内存中获取数据,获取到i=5,即读到了不一致的数据
写传播 & 事务串行化
为了解决上述问题,提出了写传播和事务串行化。
写传播:
在处理器1中的A核心的操作,可以传播到B核心和其他处理器中。
主要是通过总线嗅探bus snooping的方式,即监听是否有数据变的。
事务串行化:
多个处理器对同一个值进行修改,在同一时刻只能有一个处理器写成功,必须保证写操作的原子性,多个写操作必须串行执行。
主要是通过lock指令来实现。
MESI协议
上述写传播中,我们并不需要把数据广播到所有处理器中,因为有时候用不上,所以提出了MESI协议。
主要原理:通过总线嗅探策略(将读写请求通过总线广播给所有核心,核心根据本地状态进行响应)
状态:
- Modified(M):已修改,某数据已修改但是没有同步到内存中。如果其他核心要读该数据,需要将该数据从缓存同步到内存中,并将状态转为 S。
- Exclusive(E):某数据只在该核心当中,此时缓存和内存中的数据一致。
- Shared(S):某数据在多个核心中,此时缓存和内存中的数据一致。
- Invaliddate(I):某数据在该核心中以失效,不是最新数据。
事件:
- PrRd:核心请求从缓存块中读出数据; Process Read
- PrWr:核心请求向缓存块写入数据;
- BusRd:总线嗅探器收到来自其他核心的读出缓存请求;
- BusRdX:总线嗅探器收到另一核心写⼀个其不拥有的缓存块的请求;
- BusUpgr:总线嗅探器收到另一核心写⼀个其拥有的缓存块的请求;
- Flush:总线嗅探器收到另一核心把一个缓存块写回到主存的请求;
- FlushOpt:总线嗅探器收到一个缓存块被放置在总线以提供给另一核心的请求,和 Flush 类似,但只不过是从缓存到缓存的传输请求。
状态机:
写后锁住ME状态,避免相关内存的访问
1.4 原子性的实现
原子操作:多线程环境下,确保对共享变量的操作在执行时不会被干扰,从而避免竞态条件。
对于多处理器多核心,要保证原子性:
- 操作指令不被打断(关中断)
- lock指令只需阻止其他核心对相关内存空间的访问
- MESI协议
2 锁
2.1 互斥锁
独占锁,当线程A获得锁后,线程B无法获得锁,只有A释放锁后才能获得锁。
互斥锁在被占用时,其他线程先在用户态自旋一会,若获取锁失败,会任务挂起,进入阻塞队列(内核中)。
锁释放后,线程从阻塞队列中取出,转换为就绪态。
2.2 自旋锁
自旋锁在发生资源冲突时,原地等待,转换为就绪态。
2.3 原子变量
std::atomic<T>
:声明原子变量is_lock_free
:是否支持无锁操作,只有atomic_flag是无锁操作,只有true或flag,对应一个字节store(T desired, std::memory_order order)
:用于将指定的值存储到原子对象中load(std::memory_order order)
:用于获取原子变量的当前值exchange(std::atomic<T>* obj, T desired)
:obj
参数指向需要替换值的atomic对象,desired
参数为期望替换成的值。如果替换成功,则返回原来的值。compare_exchange_weak(T& expected, T val, memory_order success, memory_order failure)
:比较一个值和一个期望值是否相等,如果相等则将该值替换成一个新值,并返回 true;否则不做任何操作并返回 false。
注意,compare_exchange_weak 函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败并重试。如果需要保证严格的原子性,则应该使用 compare_exchange_strong 函数。compare_exchange_strong(T& expected, T val, memory_order success, memory_order failure)
3 内存序
由于编译器有时候会对指令进行优化重排,CPU也会对指令进行重排,但有时候可能产生问题。
内存序规定了多个线程访问同一个内存地址时的语义
- 某个线程对内存地址的更新何时能被其它线程看见
- 某个线程对内存地址访问附近可以做怎么样的优化
memory_order_relaxed
松散内存序,只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用
memory_order_release
在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去
memory_order_acquire
在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去
memory_order_acq_rel
一个读‐修改‐写操作同时具有获得语义和释放语义,即它前后的任何读写操作都不允许重排
memory_order_seq_cst
顺序一致性语义
对于读操作相当于获得,对于写操作相当于释放,对于读‐修改‐写操作相当于获得释放
是所有原子操作的默认内存序,并且会对所有使用此模型的原子操作建立一个全局顺序,保证了多个原子变量的操作在所有线程里观察到的操作顺序相同,当然它是最慢的同步模型。