世界上最简单的无锁哈希表

2023-05-16

英文原文:preshing,感谢@浅水清流 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线。以下是译文。

——————————————————

无锁哈希表(Lock-Free Hash Table )可以提高多线程下的性能表现,但是因为实现一个无锁哈希表本身的复杂度不小。(ps:真正的复杂在于出错之后的调试,因为多线程下的调试本身就很复杂,引入无锁数据结构之后,传统的看堆栈信息和打印log都基本上没有意义了。堆栈中的数据可能被并发访问破坏,而打印log本身可能会改变程序执行时对数据访问的时序。一个比较可行的做法是实现一个无锁版本和一个传统数据结构+锁的版本,出错后通过替换来定位是无锁数据结构本身的bug还是其他逻辑的bug)。所以对一个项目而言,无锁数据结构基本上是一把双刃剑。

据我所知,第一个用于实际开发的无锁哈希表是 Dr. Cliff Click 为Java而写。在2007年他发布了这个无锁哈希表的源码并且在google做了关于它的报告(视频)。我承认,在我第一次看这个报告的时候,我对它的大部分内容都不理解。Dr. Cliff Click是这个领域的先驱。(Maged M. Michael 在IBM做了大量关于无锁数据结构的研究。这个是2002年的一篇论文,关于哈希表,http://www.research.ibm.com/people/m/michael/spaa-2002.pdf)

很幸运,6年时间足够我理解Dr. Cliff Click所做的研究。事实上,你不必做一些前沿的探索,去实现一个完美的无锁哈希表。在这里我将分享我实现的这个版本。我相信有使用C++进行多线程开发经验的程序员,可以通过这篇博客梳理以前的经验,并且完全理解它。

 

约束

作为一个程序员,平时我们实现一个数据结构会本能的尽可能通用。这不是一件坏事,但是当我们把通用当作一个更重要的目标时,它可能会阻碍我们。在这里我走向另一个极端,实现了一个尽可能简单的,仅用于一些特殊环境的哈希表,下面是它的设计约束:

  1. table 只接受类型为32位整数的key和value
  2. 所有key必须非零
  3. 所有的value必须非零
  4. table的最大数目固定且必须是2的幂
  5. 唯一可用的操作是SetItem和getItem
  6. 有没有删除操作

当然你掌握了这种算法实现机制之后,可以在此基础上进行扩展,而不受这些限制的约束。(rehash,删除和遍历,这些都会增加复杂度,而且有引发新的ABA问题的可能性)。

 

实现方法

有很多种方法来实现一个哈希表。这里我选择了用我以前的帖子中描述的ArrayOfItems类做一个简单的修改,(前置扩展阅读) A Lock-Free… Linear Search?

这个哈希表被我称为HashTable1,和ArrayOfItems一样,它采用了一个巨大的key-value pairs数组实现。

struct Entry
{
    mint_atomic32_t key;
    mint_atomic32_t value;
};
Entry *m_entries;


在hashtable1中,仅仅只有数组本身而没有使用链接来处理碰撞。数组全部初始化为0,key为0时对应的节点为空。插入时,会通过线性搜索找到一个空节点。

ArrayOfItems和HashTable1之间唯一的区别是,ArrayOfItems是从0开始做线性搜索,而HashTable1使用MurmurHash3′s integer finalizer算法得到一个hash值,然后以这个hash值为起点开始搜索()

inline static uint32_t integerHash(uint32_t h)
{
    h ^= h >> 16;
    h *= 0x85ebca6b;
    h ^= h >> 13;
    h *= 0xc2b2ae35;
    h ^= h >> 16;
    return h;
}


当我们使用相同的key做参数调用SetItem或GetItem方法时,它会在相同的index开始做线性搜索,而使用不同的key时,会在不同的index开始搜索。通过这种方式,可以提高查找到对应key所在节点的速度,并且保证多线程并发调用SetItem或GetItem的安全性。

HashTable1采用环形的搜索,当搜索到尾部时,会从数组头部开始继续搜索。在数组满之前,每次搜索都可以保证返回对应key所在的节点,或者是一个空节点。这种技巧被称为open addressing with linear probing,,在我看来这无疑是对lock-free最友好的hash算法,事实上在Dr. Cliff Click为java实现的哈希表中也使用了相同的技巧。

 

代码

SetItem的实现。它会扫描整个数组并且将value保存在与key对应的节点或空节点。这段代码与ArrayOfItems:: SetItem几乎相同,唯一的区别是计算了hash值并且按位与,保证index在数组边界内。

void HashTable1::SetItem(uint32_t key, uint32_t value)
{
    for (uint32_t idx = integerHash(key);; idx++)
    {
        idx &= m_arraySize - 1;
 
        uint32_t prevKey = mint_compare_exchange_strong_32_relaxed(&m_entries[idx].key, 0, key);
        if ((prevKey == 0) || (prevKey == key))
        {
            mint_store_32_relaxed(&m_entries[idx].value, value);
            return;
        }
    }
}


GetItem的实现也同样和ArrayOfItems::GetItem有类似的改变。

 

uint32_t HashTable1::GetItem(uint32_t key)
{
    for (uint32_t idx = integerHash(key);; idx++)
    {
        idx &= m_arraySize - 1;
 
        uint32_t probedKey = mint_load_32_relaxed(&m_entries[idx].key);
        if (probedKey == key)
            return mint_load_32_relaxed(&m_entries[idx].value);
        if (probedKey == 0)
            return 0;         
    }
}


上述功能都是线程安全的,无锁的ArrayOfItems出于同样的原因:对数组的元素采用原子操作,使用 cas 操作修改节点的key值(使用内存栅障保证线程安全,事实上就是重新排列了内存访问指令的执行次序)。在上一篇中有更详细的讨论。

最后,就像在以前的帖子中,我们可以优化SetItem,第一次判断是否可以避免使用CAS操作。如下这种优化,可以使示例应用程序运行快大约20%。

 

void HashTable1::SetItem(uint32_t key, uint32_t value)
{
    for (uint32_t idx = integerHash(key);; idx++)
    {
        idx &= m_arraySize - 1;
 
        // Load the key that was there.
        uint32_t probedKey = mint_load_32_relaxed(&m_entries[idx].key);
        if (probedKey != key)
        {
            // The entry was either free, or contains another key.
            if (probedKey != 0)
                continue;           // Usually, it contains another key. Keep probing.
 
            // The entry was free. Now let's try to take it using a CAS.
            uint32_t prevKey = mint_compare_exchange_strong_32_relaxed(&m_entries[idx].key, 0, key);
            if ((prevKey != 0) && (prevKey != key))
                continue;       // Another thread just stole it from underneath us.
 
            // Either we just added the key, or another thread did.
        }
 
        // Store the value in this array entry.
        mint_store_32_relaxed(&m_entries[idx].value, value);
        return;
    }
}


这个就是几乎可以精简的最简单的无锁哈希表,这里是它的代码链接: source and header。

一个忠告:与ArrayOfItems一样,HashTable1上的所有操作都采用了relaxed memory ordering做限制。因此,当在HashTable1中设置标记,共享一些数据供其他线程访问时,必须事先插入release fence。同样访问数据的线程在调用GetItem前需要acquire fence。

// Shared variables
char message[256];
HashTable1 collection;
 
void PublishMessage()
{
    // Write to shared memory non-atomically.
    strcpy(message, "I pity the fool!");
 
    // Release fence: The only way to safely pass non-atomic data between threads using Mintomic.
    mint_thread_fence_release();
 
    // Set a flag to indicate to other threads that the message is ready.
    collection.SetItem(SHARED_FLAG_KEY, 1)
}


 

简单样例

对HashTable1的一些测试对比,在上一篇帖子我做个一个类似的测试。它交替执行2个测试:一个采用2个线程,每个线程交替插入6000个key不同的item,另一个每个线程交替插入12000个key相同但是value不同的item。

 

代码放在GitHub上,你可以自己编译和执行。编译说明见README.md

在HashTable1没有满时—少于80%时—HashTable1表现的很好,我也许应该为这个说法做一些基准测试。但是以以往的常规的哈希表作为基准,我敢肯定你很难实现出性能更好的无锁哈希表。这似乎奇怪,HashTable1基于ArrayOfItems,看起来会很低效。当然,任何哈希表中,总会有发生碰撞的风险,而降阶到ArrayOfItems的风险并不为0。但是使用一个足够大的数组和类似MurmurHash3这样的hash函数,这种情况出现的很少。

在实际的工作中,我已经使用了一个和这个类似的hash-table。这是一个游戏开发的项目,我的工作是解决使用内存分配跟踪工具(memory tracker)之后对一个读写锁激烈的争用。迁移到无锁哈希表的过程非常棘手。相对HashTable1需要更复杂的数据结构,key和value都是指针而不是简单的整数。因此有必要在哈希表内部插入memory ordering。最终在此模式下运行:最坏情况下游戏的帧率提高了4-10 FPS。

 

英文原文:preshing,感谢@浅水清流 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线。

译文链接:http://blog.jobbole.com/39186/

【非特殊说明,转载必须在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】


转自:http://blog.jobbole.com/39186/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

世界上最简单的无锁哈希表 的相关文章

  • 左链接Column 'id' in field list is ambiguous

    如题错误如左链接Column 39 id 39 in field list is ambiguous 今天在写sm的时候 xff0c 用到两个表的联合查询出现的如下的错误 xff0c 仔细查找才发现原来两个表的id重复了 xff0c use
  • maven出现:Failed to execute goal on project ...: Could not resolve dependencies for project ...

    1 我的项目结构是一个父项目 xff0c 多个子项目目录如下 xff1a 2 我这里就举个例子 xff0c 所以应用的也就是core和domain这两个项目 3 两个项目都继承父项目 4 在模块中domain依赖于core xff0c 在c
  • EOS的CPU危机:BM的租赁模式或只是乌托邦

    摘要 xff1a 继RAM内存之后 xff0c EOS的CPU危机也爆发了 昨日 xff0c 由于BetDice和EOSBET为了保证游戏的运行 xff0c 占用了过多的主网CPU xff0c 导致用户资源紧张 xff0c 甚至无法转账 昔
  • 有关Shiro中Principal的使用

    1 定义 principal代表什么那 xff1f 如果阅读官方文档或者源码你会得到如下的定义 xff1a 解释 xff1a 1 xff09 可以是uuid 2 xff09 数据库中的主键 3 xff09 LDAP UUID或静态DN 4
  • 关于shiro的 subject.getPrincipal()方法

    1 说明 上一篇文章说明了 principal xff0c 而subject getPrincipal 是用来干嘛的 xff0c 他就是来获取你存储的principal xff0c 内部是怎么获取的那 xff0c 多个principal怎么
  • CentOS7 64位安装solr7.2.0

    声明 xff1a 本人为学习solr的新手 xff0c 如编写过程中有部队的地方还请各位大佬指正 本文为原创 xff0c 如要转载请注明出处 你能学到 xff1a 1 linux上solr的安装部署 xff0c 官方给出的运行方式 2 添加
  • 阿里巴巴20121009 研发/算法工程师 笔试试题【修正】

    第19题 a i 在排序后的位置是 i k i 43 k xff0c a i 43 2k 在排序后的位置是 i 43 k i 43 3k xff0c 必然有a i lt 61 a i 43 2k 所以数组a里实际上有2k个各自有序的 交错的
  • Jetpakc LiveData ViewMode详解

    前言 xff1a 本文不定时更新 xff0c 有问题欢迎在评论区提出 最近更新时间 xff1a 2022 06 21 介绍 在2017年 xff0c 那时 xff0c 观察者模式有效的简化了开发 xff0c 但是诸如RxJava一类的库有一
  • ARM64 Linux kernel + busybox rootFS via NFS over QEMU with GDB

    由于条件所限 xff0c 一般选择软件做前期模拟 xff0c 这里做一些ARM 64 Linux kernel模拟运行环境搭建工作的总结 xff0c 记录以便后用 本文只涉及kernel 43 busybox rootFS via NFS
  • 寒假学习心得--从0开始学破解

    寒假学习心得 从0开始学破解 写给和我一样将要接触或者才接触破解 的朋友们 前提 你必须得真正喜欢 她 一 工欲善其事 必先利其器 1 找一个中文版的OD PEID 记得就OD就有咱PYG版的某牛人强化版的等等等等 找一个合适的工具 干起事
  • 常用的“密码重置”代码

    61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
  • ORACLE多表查询优化

    转自某地 对作者很愧疚 不晓得地址了 ORACLE 多表查询优化 这里提供的是执行性能的优化 而不是后台数据库优化器资料 参考数据库开发性能方面的各种问题 收集了一些优化方案统计如下 当然 象索引等优化方案太过简单就不列入了 嘿嘿 执行路径
  • Word to PDF Converter v3.0 算法分析及注册机

    Word to PDF Converter v3 0算法分析及注册机 详细过程 1 xff0c 主程序在C Program Files doc2pdf DOC2PDF dll xff0c PEID查壳为ASProtect 1 23 RC1
  • Debian11连不上网络问题

    有时候可以连上 xff0c 有时候就连不上 连不上的时候 xff0c 使用ifconfig命令 xff0c 只能看到回环接口 xff0c 看不到分配的网络IP地址 最后终于解决了 xff0c 记录一下 xff0c 以防之后出现同样的问题 1
  • 安全策略调整步骤

    1 修改防火墙 xff0c 保留22 SSHD 8081 APACHE 80 关闭端口443 HTTPS 3306 MYSQL 8080 8088 53 123 2 针对PHP的BUG和安全漏洞 xff0c 只有升级版本一途 xff0c 经
  • 获取微信openid(或昵称头像)的授权登录及其代理

    lt php 本页用于微信授权登录及其代理 64 version V2 0 64 author ty1921 lt ty1921 64 gmail com gt 64 param backurl 传get参数backurl xff0c 则授
  • 常用的PHP文件头和HTML5文件头(含移动端)

    lt php PHP Header Created by ty1921 64 gmail com Ver V1 Date 2017 8 18 1 session session start 2 display errors ini set
  • VB+PHP实现在线修改Windows服务器的配置文件

    本文仅供记录 存档备案用 用途 xff1a 某电话转接系统 xff0c 需要每天修改配置文件 并重启服务端程序 原理 xff1a WEB用于展示修改界面 xff0c 提交 保存配置文件的相关数据 VB端用于定时轮训WEB上保存的数据 xff
  • 按键精灵的5级开发认证,笔试题参考

    4题是抄的 xff0c 只是为了过级 最后得93分 xff0c 可能代码还是不够最优 xff0c 有看出的大大希望能不吝指点 1 写一个脚本 xff0c 要求启动时 xff0c 记录 xff08 录制 xff09 当前鼠标的移动轨迹 xff

随机推荐

  • Linux for Ubuntu用gdebi安装deb文件

    在bantu中安装deb文件有时很不方便 xff0c 通常默认用的安装器并效果并不理想 xff0c 有时用命令吧 xff0c 太多又繁琐 所以有个软件叫GDebi xff0c 可以更加有效的帮助安装deb 首先安装gdebi程序 xff0c
  • Xshell连接后又断开问题(Disconnected from remote host)

    Last login Fri Nov 1 12 36 08 2019 from 10 0 1 25 Connection closed by foreign host Disconnected from remote host 20 0 0
  • ubuntu-16.04.6安装教程

    下载Ubuntu16 04 xff08 1 xff09 下载地址 xff1a http releases ubuntu com 16 04 记得要下载iso文件如 ubuntu 16 04 desktop amd64 iso xff0c 3
  • Hive安装详细步骤

    一 下载hive 下载hive 地址 xff1a http mirror bit edu cn apache hive 二 安装mysql 执行以下几个命令安装8 0版本mysql 1 下载MySQLyum源 xff08 8 0版本的 xf
  • LL(1)文法的语法分析java实现

    java代码如下 xff1a package 文法分析器 import java io BufferedReader import java io FileInputStream import java io InputStreamRead
  • CSDN,我的良师益友

    鲁迅曾说过 xff1a 不是缺乏天才 xff0c 而是缺乏培养天才的土壤 对于中国的 IT 行业来说 xff0c 从来不缺乏技术英雄 xff0c 缺少的是铸就技术英雄的平台 而 CSDN 就给了我们这样一个平台和机会 xff0c 所以我们是
  • 构造中小型园区网实训案例

    构造中小型园区网实训案例 一 实验工具与实验拓扑规划1 实验工具2 实验拓扑 二 需求分析三 数据规划四 实施步骤步骤1 xff1a 配置所有终端步骤2 xff1a 配置所有接入层交换机步骤3 xff1a 配置网关路由器AR1 公网路由器A
  • 软件工程复习

    第一章 xff1a 课程概述 1 1 软件危机 1 1 1 计算机软件的四个发展阶段 程序设计阶段 程序系统阶段 软件工程阶段 面向对象阶段 1 1 2 什么是软件危机 xff08 考点 xff09 软件危机是指在计算机软件的开发和维护过程
  • ArrayDeque底层实现

    一 什么是ArrayDeque 1 Deque与Queue 了解这个之前 xff0c 我们要先知道什么是Deque xff0c 它和Queue有什么区别 xff1a 在java中 xff0c Queue被定义成单端队列使用 xff0c De
  • Hive知识点汇总

    HIVE 一 Hive的优化 数据倾斜 xff1a shuffle之后Key的分布不均导致分配到Reduce端的数据不均匀 xff0c 出现个别Reduce的数据过大 xff0c 执行时间过长而出现的现象 1 数据倾斜产生的原因 xff1a
  • CentOS7安装与克隆

    CentOS7安装与克隆 一 新建虚拟机及其配置二 配置虚拟网络编辑器三 安装CentOS 7四 一些工具的安装五 虚拟机克隆六 虚拟机克隆后的配置七 配置ssh免密登陆八 批处理脚本与集群分发脚本1 将家目录配置进环境变量2 批处理脚本3
  • NGINX ./configure详解

    在 34 configure 34 配置中 xff0c with 34 表示启用模块 xff0c 也就是说这些模块在编译时不会自动构建 without 34 表示禁用模块 xff0c 也就是说这些模块在编译时会自动构建 xff0c 若你想N
  • Linux下Nginx安装使用

    一 下载解压nginx span class token comment 进入要放安装包的目录 span span class token builtin class name cd span opt software span class
  • java Collections类 详解

    目录 一 前言 二 Collections类简介 三 Collections类常用方法演示 1 static void reverse List list 代码演示 2 static void shuffle List list 代码演示
  • Activity onNewIntent注意事项

    数据上报发现 xff0c onNewIntent 以后 xff0c onResume和onPause可能不会执行 xff0c 直接执行onStop
  • Python+OpenCV实用案例应用教程:人脸检测和识别

    计算机视觉使很多任务成为现实 xff0c 其中两项任务就是人脸检测 xff08 在图像中定位人脸 xff09 和人脸识别 xff08 将人脸识别为特定的人 xff09 OpenCV实现了一些人脸检测和识别的算法 从安全到娱乐 xff0c 这
  • 基数排序 详细讲解

    1 基数排序 桶排序 介绍 基数排序 xff08 radix sort xff09 属于 分配式排序 xff08 distribution sort xff09 xff0c 又称 桶子法 xff08 bucket sort xff09 或b
  • CentOS7安装docker后服务启动不了

    问题排查 运行yum install docker后 xff0c 安装完成docker 运行 docker info 命令测试docker是否正常 则提示以下错误 xff1a Cannot connect to the Docker dae
  • Linux命令+shell脚本大全:处理损坏的包依赖关系

    有时在安装多个软件包时 xff0c 某个包的软件依赖关系可能会被另一个包的安装覆盖掉 这叫作 损坏的包依赖关系 xff08 broken dependency xff09 如果系统出现了这个问题 xff0c 先试试下面的命令 xff1a y
  • 世界上最简单的无锁哈希表

    英文原文 xff1a preshing xff0c 感谢 64 浅水清流 的热心翻译 如果其他朋友也有不错的原创或译文 xff0c 可以尝试推荐给伯乐在线 以下是译文 无锁哈希表 xff08 Lock Free Hash Table xff