两年多以前随手写了点与 lock free 相关的笔记:1,2,3,4,质量都不是很高其实(读者见谅),但两年来陆陆续续竟也有些阅读量了(可见剑走偏锋的技巧是多容易吸引眼球)。笔记当中在解决内存释放和 ABA 问题时提到了 Hazard Pointer 这个东西,有两三个读者来信问这是什么,让详细讲一下,我想了想,反正以前在看这东西的时候也记了些东西,干脆整理一下发出来。

前面写的那几篇笔记都来源于 Maged Michael 的学术论文,Hazard pointer 也是他的创想,academic paper 的特点之一就是经常有些美好的假设,关于 hazard pointer 也同样如此,以下的讨论均假设内存模型是 sequential consistent 的,否则还是问题多多。

核心问题

Hazard Pointer(以下简称为 HP) 要解决的核心问题是怎样安全地释放内存,该问题的解决在实现无锁算法时有两个关键的影响:

  1. 保证了关键节点的访问是合法的,不会导致程序尝试去读取已经释放了的内存。
  2. 保证了 ABA 问题不会出现,程序逻辑正确的前提。

这两个问题在写无锁代码时基本是无法避免的,走这条路终会遇上,多少人因此费尽心力穷尽技巧各种花样,只为把这问题彻底解决。HP 就是这众多花样各种技巧中的一种,它的做法以我的愚见也不是很完美,但实现上比较简单,不依赖具体系统,也不对硬件有特殊要求(当然 CAS 操作还是要的),从效果上看也凑和,因此无论怎样是值得参考学习的。

具体实现

在无锁算法中释放内存之所以难,主要原因在于,当一个线程准备释放一块内存时,它无法知道是否另有别的线程也同时持有该块内存的指针并需要访问,因此解决这个难点的一个直接想法就是,在每个线程获取了一个关键内存的指针后,该线程将设置一个标志,表明"我正在操作这个关键数据,你们谁都别给我随便就释放了"。当然,这个标志需要放在一个公共区域,使得任何线程都可以去读。当另一个线程想要释放一块内存时,它就去把每个线程的标志都看一下,看看是否有别的线程也在操作这块内存,从而决定是否马上释放该内存:如果有别的线程在操作该内存,则暂时不释放,等下次。具体实现如下:

  1. 建立一个全局数组 HP hp[N],数组中的元素为指针,称为 Hazard pointer,数组的大小为线程的数目,即每个线程拥有一个 HP。
  2. 约定每个线程只能修改自己的 HP,而不允许修改别的线程的 HP,但可以去读别的线程的 HP 值。
  3. 当线程尝试去访问一个关键数据节点时,它得先把该节点的指针赋给自己的 HP,即告诉别人不要释放这个节点。
  4. 每个线程维护一个私有链表(free list),当该线程准备释放一个节点时,把该节点放入自己的链表中,当链表数目达到一个设定数目 R 后,遍历该链表把能释放的节点通通释放。
  5. 当一个线程要释放某个节点时,它需要检查全局的 HP 数组,确定如果没有任何一个线程的 HP 值与当前节点的指针相同,则释放之,否则不释放,仍旧把该节点放回自己的链表中。

HP 算法主要用在实现无锁的队列上,因此前面的具体步骤其实基于以下几个假设:

  1. 队列上的元素任何时候,只可能被其中一个线程成功地从队列上取下来,因此每个线程的 free list 中的元素肯定是唯一的。
  2. 线程在操作无锁队列时,任何时候基本只需要处理一个节点,因此每个线程只需要一个 HP 就够了,如果有特殊需求,当然 HP 的数目也可以相应扩展。
  3. 对于某个节点来说,多个线程同时持有该节点的指针这个现象,在时间上是非常短暂有限的,只有当这几个线程同时尝试去取下该节点,它们才可能同时持有该节点的指针,一旦某个线程成功地将节点取下,其它线程很快就会发现,并尝试继续去操作下一下节点,而后续再来取节点的线程则不再可能获得已经不在无琐队列上的节点的指针,因此:当某个线程尝试去检查其它线程的 HP 时,它只需要将 HP 数组遍历一遍就够了,不用担心各线程 HP 值的变化。

以下为我从论文里翻译过来的伪代码,入队列的函数不涉及删除节点因此不会操作 HP,难点都在处理出队列的函数上:

using hp_t = void*;

hp_t hp[N] = {0};

// 以下为队列的头指针。
node_t* top; data_t* Pop()
{
node_t* t = null;
while (true)
{
t = top;
if (t == null) break; // 设置当前线程的 HP
hp[this_thread] = t; // 以下这步是必须的,确认了当前 HP 在 t 被释放前已经被设置到当前线程的 HP 中。
if (t != top) continue; node_t* next = t->next;
if (CAS(&top, t, next)) break;
} // 已经不再持有任何节点,需将自己的 HP 设为空.
hp[this_thread] = null; if (t == null) return null data_t* data = t->data; // 尝试释放节点
DeleteNode(t); return data;
}

以上是出队列的代码,显然,所做的事情非常直白:线程拿到一个节点后将数据取出,并尝试释放节点。释放节点是另一个关键点,具体实现参看如下伪代码:

thread_local vector<hp_t> free_list;

void DeleteNode(node_t* t)
{
free_list.push_back(t);
if (free_list.size() > R) FreeNode();
} void FreeNode()
{
vector<hp_t> hp_list;
hp_list.reserve(N); // 获取所有线程的 HP,如非空则保存到 hp_list 中。
for (int i = 0; i < N; ++i)
{
if (hp[i] == null) continue; hp_list.push_back(hp[i]);
} std::sort(hp_list); vector<hp_t> not_free;
not_free.reserve(free_list.size()); // 把当前线程的 free_list 遍历遂一进行释放。
for (int i = 0;i < free_list.size(); ++i)
{
if (std::binary_search(hp_list.begin(), hp_list.end(), free_list[i]))
{
// 某个线程持有当前节点,故不能删除,还是保留在队列里。
not_free.push_back(free_list[i]);
continue;
} // 确认没有任何线程持有该节点,删除之。
delete free_list[i];
} free_list.swap(not_free);
}

存在的问题

看到这里相信读者对 Hazard Pointer 的原理已经大概了解了,那么我们来简单总结一下上面的实现。

首先是效率问题,它够快吗?根据前面的伪代码,显然影响效率的关键点在FreeNode()这个函数上,该函数有一个双重循环,但还好第二重循环用了二分查找,因此删除 R 个节点总的时间效率理论上是 O(R*logN),R 可以设置, N 是线程数目,通常也不会太大,因此均摊下来应该还好?我只能说不知道,看使用场景吧,用无琐一般有很高的效率需求,这里加个这样复杂度的处理是否会比加琐更快呢?也说不准,实现上复杂了是肯定的,想用的话得好好测试测试看看划不划得来。

其次是易用性,HP 释放节点是累进制的,只有当一个线程积累到了一定数量的节点才批量进行释放,而生产环境里通常情况复杂,会不会某个线程积累了几个节点后,就不再去队列里 pop 数据了呢?岂不是总有些节点不能释放?心里有些疙瘩。。除此,现代操作系统里线程创建销毁其实很频繁,某个线程如果要退出了,还得记得把自己头上的节点释放一下,也是件麻烦事。有人可能会觉得为什么删除节点时要把节点放到队列里再删?多此一举!直接遍历 HP 数组直到没有线程持有该节点不就好了 --- 放到队列里其实是为效率,否则每 pop 一次就遍历一遍 HP list,而且搞不好还要反复等待某个线程释放节点,均摊下来效率太低。

最后,还有一个问题,相信读者忍了很久了,HP 数组那里,各个线程怎么 index 进去取出自己的 HP 呢? thread id 吗?那这个数组不得很大很大很大?

一点改进

关于 HP 数组的实现上,作者其实也看到了问题,提出可以用 list 来管理 HP,因为不是每个线程都必须固定分配一个 HP,事实上只有当该线程正在进行 pop 操作的时候它才需要,pop 完了马上就可以把 HP 还回去了,因此数组可以用链表来替换,当然这个链表也得是 Lock free 的,但这个链表可以不用考虑回收和释放实现上容易多了,和我在本系列文章的第四篇里提到的思路是一致的。

但这样用 List 来代替数组在一定程度也增加了效率负担,因为每个线程取出 HP 变得更慢了(首先是很容易引起多个线程冲突,其次用到了 CAS 以及函数调用的开销),当然具体有多少效率损失还得看使用场景,需要好好测量一下---写无琐代码不能少做的事情。

无琐编程很难,但这并不代表它们因此只能是理论游戏,Maged Michael 的无琐系列文章启发了很多人,这其中也包括 c++ 里的大腕 Andrei Alexandrescu,呐呐,看这里

实现无锁的栈与队列(5):Hazard Pointer的更多相关文章

  1. 基于无锁的C#并发队列实现(转载)

    最近开始学习无锁编程,和传统的基于Lock的算法相比,无锁编程具有其独特的优点,Angel Lucifer的关于无锁编程一文对此有详细的描述. 无锁编程的目标是在不使用Lock的前提下保证并发过程中共 ...

  2. 基于无锁的C#并发队列实现

    最近开始学习无锁编程,和传统的基于Lock的算法相比,无锁编程具有其独特的优点,Angel Lucifer的关于无锁编程一文对此有详细的描述. 无锁编程的目标是在不使用Lock的前提下保证并发过程中共 ...

  3. boost 无锁队列

    一哥们翻译的boost的无锁队列的官方文档 原文地址:http://blog.csdn.net/great3779/article/details/8765103 Boost_1_53_0终于迎来了久 ...

  4. Boost无锁队列

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/okiwilldoit/article/details/50970408 在开发接收转发agent时, ...

  5. 无锁队列以及ABA问题

    队列是我们非常常用的数据结构,用来提供数据的写入和读取功能,而且通常在不同线程之间作为数据通信的桥梁.不过在将无锁队列的算法之前,需要先了解一下CAS(compare and swap)的原理.由于多 ...

  6. HashMap的原理与实 无锁队列的实现Java HashMap的死循环 red black tree

    http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html https://zh.wikipedia.org/wiki/%E7%BA ...

  7. zeromq源码分析笔记之无锁队列ypipe_t(3)

    在上一篇中说到了mailbox_t的底层实际上使用了管道ypipe_t来存储命令.而ypipe_t实质上是一个无锁队列,其底层使用了yqueue_t队列,ypipe_t是对yueue_t的再包装,所以 ...

  8. 一个可无限伸缩且无ABA问题的无锁队列

    关于无锁队列,详细的介绍请参考陈硕先生的<无锁队列的实现>一文.然进一步,如何实现一个不限node数目即能够无限伸缩的无锁队列,即是本文的要旨. 无锁队列有两种实现形式,分别是数组与链表. ...

  9. 无锁队列--基于linuxkfifo实现

    一直想写一个无锁队列,为了提高项目的背景效率. 有机会看到linux核心kfifo.h 原则. 所以这个实现自己仿照,眼下linux我们应该能够提供外部接口. #ifndef _NO_LOCK_QUE ...

随机推荐

  1. 注册asp.net

    %windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -i

  2. javaweb学习总结(二十六)——jsp简单标签标签库开发(二)

    一.JspFragment类介绍 javax.servlet.jsp.tagext.JspFragment类是在JSP2.0中定义的,它的实例对象代表JSP页面中的一段符合JSP语法规范的JSP片段, ...

  3. 部分设备在微信内无法播放audio的解决方案

    临时接到一个紧急的需求,一个活动页面,在某台iPhone 5S设备上无法播放音频,其它设备均正常.我接到这个任务时,也是一脸懵逼,试过在audio标签上添加controls属性来显示audio,结果发 ...

  4. Oracle的FRA(Flash Recovery Area)的好处

    如果FRA的空间耗尽,只会影响到这个Oracle实例自身.所以不会耗尽所有磁盘空间从而影响到其它的数据库实例或其它应用.

  5. DataGridView很详细的用法(转载)

    一.DataGridView 取得或者修改当前单元格的内容: 当前单元格指的是 DataGridView 焦点所在的单元格,它可以通过 DataGridView 对象的 CurrentCell 属性取 ...

  6. 努力学习 HTML5 (2)—— 元素的增和删

    HTML5 放松了某些规则,HTML5 的制定者想让这门语言更紧密地反映浏览器的现实. 放松的规则 不要求包含 <html>.<head> 和 <body> 元素. ...

  7. Zone.js 简介 & 抛砖引玉

    Zone.js是angular团队参照NodeJS的Domain,Dart的Zone,为angular 2开发的核心组件. 一开始,我对Zone.js是拒绝的.我们知道类似的 Domain 模块,主要 ...

  8. Web - 客户端存储的几种方式

    客户端存储主要方便一些APP离线使用.今天就来说说客户端存储的方法有多少? 说在最前面的一句:所有的客户端存储都有一个原则:读写的数据必须要同域 1 Cookie Cookie是一项很老的技术的,就是 ...

  9. Android开发(二十六)——Application

    application package com.lgaoxiao.application; import java.util.LinkedList; import java.util.List; im ...

  10. 博为iHospital-HIS医院信息系统产品系列

    博为软件iHospital-HIS产品系列式面向大中型医院应用的完整医院流程信息化产品,覆盖了医院主要的业务流程,管理职能,和病人在医院诊疗的各个环节.将医院流程的优化与软件系统完美结合,为建立数字化 ...