常用lock-free编程

RCU(COW)

RCU 的关键思想有两个:1)COW,复制后更新;2)延迟回收内存。典型的RCU更新时序如下:

复制:将需要更新的数据复制到新内存地址;
更新:更新复制数据,这时候操作的新的内存地址;
替换:使用新内存地址指针替换旧数据内存地址指针(内存地址替换操作是原子的),此后原有读者访问旧数据,新的读者将访问新数据;
等待: 所有访问旧数据的读者进入静默期(QS),说明之前的reader都离开了临界区, 这个时候访问旧数据完成;
回收:当所有的CPU都至少经历过一次QS后, 意味着宽限期的结束,因此就进行回收处理工作;

基本概念

读临界区
RCU读者执行的区域,每一个临界区开始于rcu_read_lock(),结束于rcu_read_unlock(),读临界区中使用rcu_dereference()获取一个受RCU保护的指针
写临界区
写侧使用rcu_assign_pointer来对受RCU保护的指针赋新值, 推迟销毁被RCU保护的旧值
静默期
静默态(quiescent state): 当一个线程没有运行在读侧临界区时,其就处在静默状态。
宽限期
当所有的CPU都至少经历过一次QS后,宽限期将结束并触发回收工作。在不支持抢占的RCU实现中,检测到CPU有context切换,就能表明CPU离开了临界区,因此读者在持有rcu_read_lock()的时候,不能发生进程上下文切换

相关API

rcu_read_lock()  //标记读者临界区的开始
 rcu_read_unlock()  //标记读者临界区的结束
synchronize_rcu() / call_rcu() //等待Grace period结束后进行资源回收
rcu_assign_pointer()  //Updater使用这个宏对受RCU保护的指针进行赋值
rcu_dereference()  //Reader使用这个宏来获取受RCU保护的指针

COW+Reclaimer
在这里插入图片描述

Reader
使用rcu_read_lock和rcu_read_unlock来界定读者的临界区,访问受RCU保护的数据时,需要始终在该临界区域内访问;
在访问受保护的数据之前,需要使用rcu_dereference来获取RCU-protected指针;当使用不可抢占的RCU时,rcu_read_lock/rcu_read_unlock之间不能使用可以睡眠的代码;

Updater
多个Updater更新数据时,需要使用互斥机制进行保护;
Updater使用rcu_assign_pointer来移除旧的指针指向,指向更新后的临界资源;
Updater使用synchronize_rcu或call_rcu来启动Reclaimer,对旧的临界资源进行回收,其中synchronize_rcu表示同步等待回收,call_rcu表示异步回收;

Reclaimer
Reclaimer回收的是旧的临界资源;
为了确保没有读者正在访问要回收的临界资源,Reclaimer需要等待所有的读者退出临界区,这个等待的时间叫做宽限期(Grace Period);

具体例子

在这里插入图片描述

上图中,每一个读者、更新者表示一个独立的线程,总共4个读线程,一个写线程。

RCU更新操作分为两个阶段:移除阶段和回收阶段。两个阶段通过宽限期隔开。更新者在移除元素后,通过synchronize_rcu()原语,初始化一个宽限期,并等待宽限期结束后,回收移除的元素。

  1. 移除阶段:RCU更新通过rcu_assign_pointer()等函数移除或插入元素。现代CPU的指针操作都是原子的, rcu_assign_pointer()原语在大多数系统上编译为一个简单的指针赋值操作。移除的元素仅可被移除阶段(以灰色显示)前的读者访问。
  2. 回收阶段:一个宽限期后, 宽限期开始前的原有读者都完成读操作,因此,此阶段可安全释放由删除阶段删除的元素。

缺点

RCU不支持临界区阻塞,因为这个时候会发送CPU上下文切换.

RCU参考链接
rcu 机制简介
Linux RCU原理剖析(一)-初窥门径
Linux RCU原理剖析(二)-渐入佳境
RCU(Read Copy Update)十年计

Hazard Point(COW)

基本概念

所有读线程维持一个全局链表Hazard Pointer List,每个Hazard Pointer有一个成员flag标识其是否可用,一个指针ptr表示被保护的原始指针. 读的时候找到一个空闲节点(没有则新建一个)并将flag设置为true, ptr指向被保护的指针对象. 读完后flag设置为false,ptr置空. 通过CAS保证链表线程安全.

每个写线程维持一个thread-local的retire-list, 当retire-list达到一定阈值后触发内存回收,找出retire-list和Hazard Pointer List的补集然后将这些指针释放.

缺点

需要为每个共享变量维护一个线程的hazard pointer,对于有遍历需求的数据结构同时存在的hazard pointer很容易膨胀.

Hazard Point参考链接
Lock-free 编程:A Case Study(下)
并行编程中的内存回收Hazard Pointer

Hazard Version(COW)

基本概念

Hazard Version数据结构主要由三部分组成

  1. 全局的Current Version变量
  2. 每个线程局部一个Hazard Version变量
  3. 每个线程局部一个待回收的内存块链表Retire List

Hazard Version提供如下三个操作逻辑:

  1. 每个线程要开始访问“可能被其他线程释放”的内存块前,将当前Current Version的值保存在线程局部的Hazard Version中,对共享内存块的操作完成后,再清除线程局部的Hazard Version值;
  2. 要释放一个共享内存块时,原子的将Current Version加1后,将旧值保存在内存块中,然后将它挂在线程局部的RetireList上;
  3. 当待回收的内存块过多时,遍历所有线程的Hazard Version,以及全局的Current Version,获得最小值(MinVersion),遍历待回收的内存块,将Version小于Min Version的内存块回收掉(所有线程的Hazard Version都大于需要释放内存快的Hazard Version)。

缺点

管理多个线程Hazard Version的“滑动窗口”,太久不释放的Hazard Version,会阻塞内存回收。在使用时需要注意,对Hazard Version的require和release操作之间,不能有耗时过长的逻辑。

Hazard Version参考链接
[LockFree之美] 使用Hazard Version实现的无锁Stack与Queue

内存屏障

CPU缓存一致性(可见性)

缓存一致性通过MESI协议保证,指的是cpu的L1缓存(L1,L2独有,L3共享,再下面就是主存)。
MESI状态流转

Store Buffere—存储缓存

store buffer即存储缓存。位于内核和L1缓存之间。当处理器需要处理将计算结果写入在缓存中处于shared状态的数据时,需要通知其他内核将该缓存置为 Invalid(无效),引入store buffer后将不再需要处理器去等待其他内核的响应结果,只需要把修改的数据写到store buffer,通知其他内核,然后当前内核即可去执行其它指令。当收到其他内核的响应结果后,再把store buffer中的数据写回缓存,并修改状态为M。

Invalidate Queue—失效队列

处理器修改数据时,需要通知其它内核将该缓存中的数据置为Invalid(失效),我们将该数据放到了Store Buffere处理。那收到失效指令的这些内核会立即处理这种失效消息吗?答案是不会的,因为就算是一个内核缓存了该数据并不意味着马上要用,这些内核会将失效通知放到Invalidate Queue,然后快速返回Invalidate Acknowledge消息(意思就是尽量不耽误正在用这个数据的内核(Store Buffer那个核)正常工作)。后续收到失效通知的内核将会从该queue中逐个处理该命令。

指令重排(有序性)

只要没有依赖,代码中在后面的指令就可能跑到前面去,编译器和CPU都会这么做。不管指令如何重排,对单线程来说,结果必然是一致的, 即不会改变单线程程序的行为.

// 代码片段6
a = 1;
b = 2;
c = a + b;
  • 编译器/CPU/VM 可以对a = 1;和b = 2;进行对换,而不能将c = a + b与前面两句对换.
  • 对指定地址的操作(读写)序列,CPU是会保证和程序顺序一致的(比如a是先写后读),并且CPU的读写对自己总是可见的(Store Forwarding).
  • CPU不能解析多线程的依赖关系,可能会乱序执行,比如如果有其它线程依赖于a先于b赋值这个事实,那么就必须要应用程序告诉CPU/编译器,a和b有依赖关系,不要重排。

atomic

合理的指令重排在单线程环境下不会造成逻辑错误, 但多线程不能解析依赖关系可能会造成逻辑错误。memory order本身与多线程无关,通过限制单一线程当中指令执行顺序以及对cache可见性来解决多线程环境下出现的这个问题。以atomic操作为边界,根据对其之前的内存访问命令、之后的内存访问命令能够在多大的范围内自由重排形成6种内存序。内存序封装了内存屏障功能,保证有序性和可见性的顺序,解决了指令重排和cpu缓存一致性问题;atomic同时通过cas解决原子性问题。
六种内存序实际上对应三种内存模型,具体如下:
在这里插入图片描述
apache/incubator-brpc
memory_order_relaxed
除了保证操作的原子性之外,没有限定前后指令的顺序
acquire-release
主要包括memory_order_acquire(类似于mutex的lock), memory_order_release(类似于mutex的unlock), memory_order_consume(依赖型acquire),memory_order_acq_rel(acquire + release语意)。

  • memory_order_release: 原子操作之前的读写不能往后乱序;并且之前的写操作,会被使用acquire/consume的线程观察到。这里要注意它和seq_cst不同的是只有相关的线程(使用acquire或consume模式加载同一个共享变量的线程)能观察到,而seq_cst是所有seq_cst模式的线程(无论是否加载同一个变量)都能观察到。
  • memory_order_acquire: 原子操作之后的读写不能往前乱序;它能看到release线程在调用load之前的那些写操作。
  • memory_order_consume:原子操作之后的的依赖变量读写不能往前乱序;它可以看到release线程在调用load之前那些依赖变量的写操作。依赖是指和该原子变量有依赖关系的变量。
  • release/acquire的效率要比seq_cst高,常用于spinlock和读写锁的实现。

memory_order_seq_cst
该原子操作前后的读写不能跨过该操作乱序,该原子操作之前的写操作都能被所有memory_order_seq_cst线程观察到且具有唯一试图。
表象上看就是每个处理器的执行顺序和代码中的顺序一样;所有memory_order_seq_cst模式处理器都只能看到一个单一的操作执行顺序,就像单线程一样。

顺序一致性:几个线程观察到一个事件的时机可能不同,但一旦观察到,事件内部的顺序肯定是一致的。

如何理解 C++11 的六种 memory order?

Memory Barrier

内存屏障保证有序性和可见性的有序性。即栅栏后的操作不会在栅栏之前发生,且栅栏前的操作结果对栅栏后的操作必然是可见的。
可见性保证
比如原子变量A被读线程和写线程共享,此时写线程写A。内存屏障会让写线程将Store Buffer中的新数据flush到L1 cacheline(S——M),稍后会writeback回内存中;同时让读线程消费Invalidate Queue中的无效通知使L1中对应的cacheline无效(S——I), 接下来读线程读对应原子变量时会发生cachemiss,写线程会将L1 cacheline中的数据writeback回内存,读线程从主存中获取最新值,获取完后读线程和写线程的cacheline变为S状态。
有序性保证
屏障前的操作肯定在屏障后的操作之前完成。

CAS与ABA问题

  1. CAS是通过硬件以及cpu指令实现简单对象的原子操作(包括多线程写)。当修改的数据在同一个cache-line时通过MESI协议和内存屏障来保证,当修改的数据不在同一个cache-line通过锁总线来保证。
  2. CAS本身没有ABA问题,ABA问题的出现是因为对CAS的使用方式
  3. 对于CAS产生的ABA问题,通常的解决方案是采用CAS的一个变种DCAS。DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后再这个变量需要update的时候,就同时检查变量的值和计数器的值。

CAS实现无锁链表

其他

上篇|说说无锁(Lock-Free)编程那些事
下篇|说说无锁(Lock-Free)编程那些事

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值