cache
cache表示的是高速缓冲区
为什么要有cache?
为了更好的利用局部性原理,减少cpu访问主存的次数,提升效率,以及解决主存和cpu运行速度不匹配的问题。
首先我们先了解我们的内存结构
cpu->寄存器->一级缓存->二级缓存->三级缓存->主存。
离cpu的位置越来越换,获取的速度越来越慢。而cpu获取内存的最小单位为cache line。
cache是由cache line(缓存行)组成的。分为三个区域flag、tag、以及data,其中flag中存的是缓存一致性协议中的值,用来标记是否可见。而tag是用来记录辅助缓存地址定位的。data用来存储。
在cpu中有寄存器、一级缓存、二级缓存、三级缓存,cache是缓存的总和;
补充:缓存命中 通俗的意思就是找到自己需要的内存,优先级一级->二级->三级->主存
缓存一致性协议MESI
为什么需要缓存一致性协议?
实现原子性
在cup运行的过程中可能在内存中修改、添加、删除等操作(基于写回策略),以往cpu操作完成之后都会将数据同步更新到主存,保证可见性,但这样很影响性能;
优化为主存的数据可以和cpu中的数据不一样,如果是写操作缓存命中的情况下直接写入缓存,然后将数据标记为脏数据,如果没有命中,用LRU(最近最少使用)的策略找到一块内存,如果是脏数据刷到主存中去,不是脏数据从内存中读取数据,然后写,标记为脏数据。读的情况类似。
对于单核来说这样没有问题,对于多核,就会造成缓存不一致的问题,一种就是回到之前的写一次,刷一次主存。还有一种是cup遵循监听发布者模式(总线嗅探机制),将事务穿行化,基于lock指令锁总线,有cup操作时候就在总线上广播,其他cpu也就更新,但是这样也会造成资源的浪费(另一个cup不会用到这个数据,也广播)。如何解决这个问题,就用到了MESI一致性协议,也是基于总线嗅探机制实现了事务的串行化,通过状态降低了总线带宽的压力,有了标记就不用每个都锁总线了,对于一些别的cpu没有的数据,对相关内存lock指令锁住就行。
原子性
原子性就是不是0就是1,是一个状态变成另一个状态,不会有中间状态。
要实现原子性要保证操作的指令不会被打断,底层的自旋锁既可以实现。
内存序
有了原子性我们再关注内存序的问题
为什么有内存序?
因为编译器优化重排和cpu指令重排
内存序规定了什么?
规定了多个线程访问同一个内存地址时的语义、某个线程对内存的更新何时能被其他线程看见,某个线程对内存地址访问附近可以做什么优化。
有了内存序,我们可以通过原子变量的原子操作指导如何对代码经行优化,在并发编程时保证逻辑的正确性
原子操作
队原子变量的操作为原子操作
常用的原子操作有
is_lock_free
store
load
exchange
compare_exchange_weak
compare_exchange_strong
fetch_add
fetch_sub
fetch_or
fetch_xor
我们经常通过compare_exchange、exchange、fetch_add等原语操作实现无锁/无等待消息队列
无锁/无等待消息队列
无锁队列(lock-free)/无等待队列(wait-free)是相关但不完全相同的2个概念
lock-free只保证整个系统整体无论如何向前移动,不能保证每个线程的前进速度(可能出现线程饿死),常使用compare_exchange原语实现,可以有循环,但类似compare_exchange实现的自旋锁不行。
wait-free保证每个线程在有限步骤中向前移动,常使用exchange、fetch_add等原语实现,且不包含可能被其他线程影响的循环。
什么情况只能使用无锁消息队列?
1.信号处理程序,应为信号能随时打断当前线程切换到其他线程,可能导致当前线程的锁没有释放,但是其他线程去申请锁,导致死锁。
2.实时系统
什么时候使用无锁队列?
我们使用队列先用有锁的实现,判断是否要保证系统向前移动,再用无锁队列优化,如果不需要保证向前移动,则比较谁的性能高就用谁。
如果任务执行的时间断,用无锁队列,进行长时间的运算,耗时io则用有锁队列。
消息队列
其包括了MPMC\SPMC\MPSC\SPSC
这里介绍ProducerConsumerQueue和MsgQueue2种
ProducerConsumerQueue中主要由锁,队列,条件变量构成。能用到多线程中。
其的缺点为锁的碰撞比较多,生产者vs生产者、消费者vs消费者、生产者vs消费者都有碰撞
如何优化?
MsgQueue其中对生产者队列和消费者队列进行了分离的操作,且结构中有2个锁,读锁和写锁,
这样就能减少读和写的碰撞,只有当取数据且队列中没有数据的时候,才会进行读和写的碰撞(读写队列的切换)。
队列的实现
队列实现可以基于数组和链表
基于数组的优点:可以快速的随机寻找数据,通常更快,但是不是严格无锁的
缺点:需要预先分配内存,扩容耗时。
基于链表的优点:不用预先分配内存,在首尾添加元素快
缺点:容易导致内存碎片,造成内存的浪费,查找元素的效率低。
混合链表和数组的队列
结合2中结构的优点。
对于链表有侵入式和非侵入式
侵入式表示由外部构建的node插入队列之中,这样队列就不用对node经行内存的管理,由外部管理
非侵入式表示由外部传入数据,内部构建node节点,然后插入队列中去。
同时队列的实现要是有界的,无界队列非常的危险。
因为要并发编程,所以我们用CAS(compare and swap)代替锁来实现无锁队列
CAS锁
例如将索引队列的指针或者索引设置成原子变量,那么就保证了队列的线程安全,例如一个线程创建一个node为input,然后_head.exchange(input,std::memory_order_acq_rel);此操作将头指针指向了你所创建的node,头指针的位置已经改变了,而且是原子的,在别的线程读到的头指针不会相同,然后这个线程后续操作将连表链接上就可以,无需上锁。