实现无锁数据结构最困难的两个问题是ABA问题和内存回收问题。它们之间存在着一定的关联:一般内存回收问题的解决方案,可以作为解决ABA问题的一种只需很少开销或者根本不需额外开销的方法,但也存在一些情况并不可行,如两个链表实现的栈,不断在两个栈间交换节点。下面对两个问题的主流解决方法进行介绍。

标签指针(Tagged pointers)

标签指针作为一种规范由IBM引入,旨在解决ABA问题。从某一方面来看,ABA问题的出现是由于不能区分前一个A与后一个A,标签指针通过标签(版本号)来解决。其每个指针由一组原子性的内存单元和标签(32比特的整数)组成。

template <typename T>
struct tagged_ptr {T * ptr ;unsigned int tag ;tagged_ptr(): ptr(nullptr), tag(0) {}tagged_ptr( T * p ): ptr(p), tag(0) {}tagged_ptr( T * p, unsigned int n ): ptr(p), tag(n){}T * operator->() const { return ptr; }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

标签作为一个版本号,随着标签指针上的每次CAS运算增加,并且只增不减。一旦需要从容器中非物理地移除某个元素,就应将其放入一个放置空闲元素的列表中。在空闲元素列表中,逻辑删除的元素完全有可能被再次调用。因为是无锁数据结构,一个线程删除X元素,另外一个线程依然可以持有标签指针的本地副本,并指向元素字段。因此需要一个针对每种T类型的空闲元素列表。多数情况下,将元素放入空闲列表中,意味着调用这个T类型数据的析构函数是非法的(考虑到并行访问,在析构函数运算的过程中,其它线程是可以读到此元素的数据)。

有了标签,A在CAS时虽然内存地址相同,但若A已被使用过,其标签不同。不会出现ABA问题。

当然其必然也有缺点:

其一:要实现标签指针需要平台支持,平台需要支持dwCAS(地址指针需一个word,标签至少需32位),由于现代的系统架构都有一套完整的64位指令集,对于32位系统,dwCAS需要64位,可以支持;但对于64位系统,dwCAS需要128位(至少96位),并不是所有架构都能支持。

The scheme is implemented at platforms, which have an atomic CAS primitive over a double word (dwCAS). This requirement is fulfilled for 32-bit modern systems as dwCAS operates with 64-bit words while all modern architectures have a complete set of 64-bit instructions. But 128-bit (or at least 96-bit) dwCAS is required for 64-bit operation mode. It isn’t implemented in all architectures.

其二:空闲列表通常以无锁栈或无锁队列的方式实现,对性能也会有影响,但也正是因为使用无锁,导致其性能有提高(相对于有锁)。 
其三: 对于每种类型提供单独的空闲列表,这样做太过奢侈难以被大众所接收,一些应用使用内存太过低效。例如,无锁队列通常包含10个元素,但可以扩展到百万,比如在一次阻塞后,空闲列表扩展至百万。

Availability of a separate free-list for every data type can be an unattainable luxury for some applications as it can lead to inefficient memory use. For example, if a lock-free queue consists of 10 elements on the average but its size can increase up to 1 million, the free-list size after the spike can be about 1 million. Such behavior is often illegal.

Example

详情可参考1

template <typename T> struct node {tagged_ptr next;T data;
} ;
template <typename T> class MSQueue {tagged_ptr<T> volatile m_Head;tagged_ptr<T> volatile m_Tail;FreeList m_FreeList;
public:MSQueue(){// Allocate dummy node// Head & Tail point to dummy nodem_Head.ptr = m_Tail.ptr = new node();}
void enqueue( T const& value )
{
E1: node * pNode = m_FreeList.newNode();
E2: pNode–>data = value;
E3: pNode–>next.ptr = nullptr;
E4: for (;;) {E5:   tagged_ptr<T> tail = m_Tail;
E6:   tagged_ptr<T> next = tail.ptr–>next;
E7:   if tail == Q–>Tail {// Does Tail point to the last element?
E8:      if next.ptr == nullptr {// Trying to add the element in the end of the list
E9:         if CAS(&tail.ptr–>next, next, tagged_ptr<T>(node, next.tag+1)) {// Success, leave the loop
E10:          break;}
E11:     } else {// Tail doesn’t point to the last element// Trying to relocate tail to the last element
E12:        CAS(&m_Tail, tail, tagged_ptr<T>(next.ptr, tail.tag+1));}}} // end loop// Trying to relocate tail to the inserted element
E13: CAS(&m_Tail, tail, tagged_ptr<T>(pNode, tail.tag+1));}
bool dequeue( T& dest ) {
D1:  for (;;) {D2:    tagged_ptr<T> head = m_Head;
D3:    tagged_ptr<T> tail = m_Tail;
D4:    tagged_ptr<T> next = head–>next;// Head, tail and next consistent?
D5:    if ( head == m_Head ) {// Is queue empty or isn’t tail the last?
D6:       if ( head.ptr == tail.ptr ) {// Is the queue empty?
D7:         if (next.ptr == nullptr ) {// The queue is empty
D8:                  return false;}// Tail isn’t at the last element// Trying to  move tail forward
D9:         CAS(&m_Tail, tail, tagged_ptr<T>(next.ptr, tail.tag+1>));
D10:      } else { // Tail is in position// Read the value before CAS, as otherwise  // another dequeue can deallocate next
D11:        dest = next.ptr–>data;// Trying to move head forward
D12:        if (CAS(&m_Head, head, tagged_ptr<T>(next.ptr, head.tag+1))
D13:           break // Success, leave the loop}}} // end of loop// Deallocate the old dummy node
D14: m_FreeList.add(head.ptr);
D15: return true; // the result is in dest}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

险象指针(Hazard pointer)

此规则由Michael创建(Hazard Pointers: Safe Memory Reclamation for Lock-Free Objects),用来保护无锁数据结构中元素的局部引用,也即延迟删除。

it’s the most popular and studied scheme of delayed deletion.

此规则的实现仅依赖原子性读写,而未采用任何重量级的CAS同步原语。其实现原理大致如下:

  1. 通过使用线程局部数据保存正在使用的共享内存(数据结构元素),以及要删除的共享内存,记为Plist,Dlist。
  2. 每个线程都将自己正在访问其不希望被释放的内存对象保存在Plist中,使用完取出。
  3. 当任何线程删除内存对象,把对象放入Dlist。
  4. 当Dlist中元素数量达到阈值,扫描Dlist和Plist,真正释放在Dlist中有而所以Plist中没有的元素。

Note:Plist 一写多读;Dlist单写单读。

hazard pointer的使用是要结合具体的数据结构的,我们需要分析所要保护的数据结构的每一步操作,找出需要保护的内存对象并使用hazard pointer替换普通指针对危险的内存访问进行保护。

Example

详情可参考3

template <typename T>
void Queue<T>::enqueue(const T &data)
{qnode *node = new qnode();node->data = data;node->next = NULL;// qnode *t = NULL;HazardPointer<qnode> t(hazard_mgr_);qnode *next = NULL;while (true) {if (!t.acquire(&tail_)) {continue;}next = t->next;if (next) {__sync_bool_compare_and_swap(&tail_, t, next);continue;}if (__sync_bool_compare_and_swap(&t->next, NULL, node)) {break;}}__sync_bool_compare_and_swap(&tail_, t, node);
}template <typename T>
bool Queue<T>::dequeue(T &data)
{qnode *t = NULL;// qnode *h = NULL;HazardPointer<qnode> h(hazard_mgr_);// qnode *next = NULL;HazardPointer<qnode> next(hazard_mgr_);while (true) {if (!h.acquire(&head_)) {continue;}t = tail_;next.acquire(&h->next);asm volatile("" ::: "memory");if (head_ != h) {continue;}if (!next) {return false;}if (h == t) {__sync_bool_compare_and_swap(&tail_, t, next);continue;}data = next->data;if (__sync_bool_compare_and_swap(&head_, h, next)) {break;}}/* h->next = (qnode *)1; // bad address, It's a trap! *//* delete h; */hazard_mgr_.retireNode(h);return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

参考资料

  1. 无锁数据结构(机制篇):内存管理规则 (EN 原文)
  2. 风险指针(Hazard Pointers)——用于无锁对象的安全内存回收机制
  3. http://blog.kongfy.com/2017/02/hazard-pointer/

无锁数据结构三:无锁数据结构的两大问题相关推荐

  1. 并发系列三:证明分代年龄、无锁、偏向锁、轻量锁、重(chong)偏向、重(chong)轻量、重量锁

    前言 上篇文章咱们了解了synchronized关键字的常见用法.对象头以及证明了一个对象在无锁状态下的对象头markwork部分的前56位存储的是hashcode.接下来,咱们继续来根据对象头分别证 ...

  2. 无锁编程(三) - 忙等待

    概念 忙等待可以认为是一种特殊的忙等待 忙等待分类 Peterson算法 xchg解法 TSL解法 自旋锁 Peterson算法 Peterson算法是一个实现互斥锁的并发程序设计算法,可以控制两个线 ...

  3. 12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

    小陈:呼叫老王...... 老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入.锁优化.和锁升级的原理 小陈:早就准备好了,我现在都等不及了 老王:那就好,那我们废话不多说 ...

  4. 无锁队列与有锁队列性能比较

    最近研究boost的无锁队列,测试了一下性能,发现无锁队列比有锁的还要慢 testqueue.cpp #include <time.h> #include <boost/thread ...

  5. 多线程:无锁、偏向锁、轻量锁、重量级锁

    一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因? --->synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有 ...

  6. [转]iPhone 港版和美版,有锁版和无锁版的区别?

    iPhone进入中国已经快两个年头了,从上市的那一天就注定会造就一批"爱疯"一族,!看来Apple的魅力还是那么光彩照人,招人喜欢!但在喜欢以前还是希望大家对iPhone要有一定的 ...

  7. MySQL - 无索引行锁升级为表锁

    文章目录 生猛干货 无索引行锁升级为表锁演示 表结构 索引信息 操作演示 结论 搞定MySQL 生猛干货 带你搞定MySQL实战,轻松对应海量业务处理及高并发需求,从容应对大场面试 无索引行锁升级为表 ...

  8. boost::graph模块实现使用不相交的集合数据结构计算无向变化的连通分量图形的测试程序

    boost::graph模块实现使用不相交的集合数据结构计算无向变化的连通分量图形的测试程序 实现功能 C++实现代码 实现功能 boost::graph模块实现使用不相交的集合数据结构计算无向变化的 ...

  9. Synchronized锁升级:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁

    一. 概述 1. Synchronized锁升级的原因 用锁能够实现数据的安全性,但是会带来性能下降.无锁能够基于线程并行提升程序性能,但是会带来安全性下降. 2. Synchronized锁升级的过 ...

最新文章

  1. SQLSERVER导入导出文本文件
  2. IOS 6 之后 Required background modes
  3. c++语言get:_用C++给R语言加速:Rcpp简单用法
  4. STM32F103_ADC-DMA通道采集
  5. CMU科学家们带一群机器人开房,并收集了28,000种不同的姿势
  6. Windows安装MySQL 5.7.19及相关问题处理
  7. Effective C++ 条款 50:了解new和delete的合理替换时机
  8. 成功通过PMP认证考试的七大要点
  9. [软件工程基础]结队项目——地铁
  10. C语言中scanf函数的用法
  11. 【读书笔记】《Web全栈工程师的自我修养》
  12. 实习僧招聘网站信息采集
  13. 利用IMU进行激光点云运动畸变校正
  14. 定时任务实现(中午12点执行,每1分钟执行一次)
  15. STM32F103c8t6超频
  16. Android安装包APK如何解压
  17. android listview仿iphone通讯录ios 3dTouch
  18. 模拟电路笔记(三)放大器
  19. PMBOK(第六版) 学习笔记 ——《第一章 引论》
  20. Karatsuba-Ofman乘法器

热门文章

  1. php实现二叉搜索树,二叉搜索树有几种实现方式
  2. linux基础系统命令大全
  3. java读取TXT文件的方法
  4. 主板usb模式启动快捷键大全
  5. css3蒙版运动,纯CSS3制作逼真的汽车运动动画
  6. php 目录管理,[PHP文件管理器]①--遍历目录
  7. while0表示什么意思_轮胎上的各种字符都表示什么意思,家用车的轮胎又该如何选择呢?...
  8. vs2010 asp.net mysql,安装VS2010后,更改iis的asp.net版本 | 吴小强的博客
  9. 微信小程序云开发 mysql_微信小程序云开发学习笔记(一)云数据库
  10. .net 怎么在控制器action中返回一个试图_一个view事件分发,面试官6连问直击灵魂,我被虐的体无完肤...