我是靠谱客的博主 忧郁人生,最近开发中收集的这篇文章主要介绍一个高性能无锁哈希表的实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

无锁哈希表(Lock-Free Hash Table)是多线程编程中的理想数据结构,但是带有并发插入和删除的实现很少见于开源项目。博主对此做了一个巧妙的设计和实现,本文于此首次阐述设计思想。

 

通过考察各种基于CAS原子操作的无锁数据结构实现,目前公认可实现无锁安全的数据结构是数组和单向队列。其他实现都一定程度上受到复杂度和ABA问题的威胁。数组的实现相对于单向队列要简单,所以无锁hash table理想的选择是数组,对于冲突不拉链。但是如何解决hash冲突呢?基本思想依然是开放寻址探测法。为了同时支持增加/查找/删除三种操作,业界各种开放寻址探测的算法,都为一次探测做了优化且照顾了其他探测次数以及最坏情况,但是这个照顾动作对于无锁设计实在是不太完美。为了达到O(1)的操作性能,开放寻址应该限制探测的次数为固定有限次,超过这个数目的探测应该通过算法降低为0,但同时用一个很小的数组(遍历模式)收容最坏情况(这里称之为保险数组)。由此可以实现,所有的操作都是对数组做无锁设计。

为了保证固定有限的探测次数极为有效(也就是够用),应该通过算法保证冲突率够低。降低冲突,一般不外乎两种措施:

一,增加hash表长度。此措施受限于内存,所以对每个bucket要尽量节省内存。一般用指针作为bucket单元去指向hash node就是很节省内存了。但64位Linux的指针是8个字节,在hash表很大而装载率较小的时候仍然是过于浪费。如果用4个字节作为bucket单元(最大可支持40亿buckets),同样的内存使用可以创造双倍hash表长度,对降低hash冲突率有很大的改进。但是4个字节是不能任意寻址的,需要把hash node有限度集中以省去最高位地址段。通过创建固定大小的内存池(一个静态数组存贮按需申请的内存块的指针,内存块按定长划分成hash node),将4个字节按位切割为两段作为hash node在内存池的两个维度的下标。请注意这里的hash node默认是固定32字节大小,一般不直接包含而是用指针去指向真正的使用者数据。为了性能提高也可以定制hash node 的大小,比如64字节(现代CPU的cache基本单元大小),这时候hash node可以直接存贮小于40字节的字符串而在cache命中。

二,选择高性能高分散性能的hash函数,以使得输出的hash值尽量接近理论上的完全随机。业界对于hash函数的讨论和实现已经有很多了,高计算性能分散性极佳的已有很多,比如murmur hash,city hash。hash函数的输出,即使达到理论上的完全随机,如果在hash表内仅仅寻找一个位置,也不能够降低冲突率。一般还需要多次探测其他位置,常用的办法就是以某个(或某些,用多个hash函数得到)位置为起点,线性或者非线性地计算出其他位置,直到找到目标位置或者循环一圈为止。但是这有两个缺陷:线性或者非线性计算都有一定的卷积(即后继位置有相关性导致随机分散性降低);后继位置数量不限制造成最坏情况过于复杂(也降低了攻击保护);

 

上面两种措施也是业界常用的降低冲突率的方案。这里增加了第三种措施去改善上面两个缺陷:如果从多个独立的随机位置开始探测有限数目的bucket,将会从理论上降低hash冲突概率。那么得到多个彼此独立的起始位置呢?作者的设计就是利用hash函数的输出 -- 选用输出128位结果的hash函数,分割成四个32位整数(128位是假定随机的因而四个整数是独立的),每个32位整数对hash table长度求模,可以得到4个独立的bucket位置,这样将大大减少冲突率。

但是,这样一般还不够好。原因是我们想尽量提hash table的装载率(load factor)且仍然能够处理冲突增加的倾向。为此,增加第二个数组,用上面四个32位整数同样地映射出第二个数组内的4个bucket位置,这样对一个key我们就有128位hash值映射到8个独立的bucket了。8个位置都探测过了仍然冲突的怎么办?扔到保险数组里面去。保险数组的长度虽然非常小,但通过概率可以保证它是够用的,除非hash函数输出不够随机(也包含key太短的原因)或者受到hash值重复攻击。对于后者当保险数组满了之后简单丢弃就达到了防卫hash攻击的后果。


至此我们有三个优化目标:1,进入保险数组的概率应该极低;2,总的bucket利用率(性能墙)尽量高;3,两个hash table数组占用的内存之和应该最小。通过计算(此略),可以限定1,得到2和3的最佳配置。在作者的实现中创建了三个数组:一个高load factor的大数组作为主hash表,一个低load factor的小数组作为辅hash表,和一个极其微小(64或128足够)的数组作为保险表。

这个优化目标需要一个前提,就是基于概率计算的前提是可靠的,也就是对于任何key,选用hash函数输出要足够随机。这个前提,对于key足够长,city hash和murmurhash函数已经做得足够好。但是对于key小于12字节或者key之间差异不够,就变差了。这个也是理论上没办法的事情。使用者应该明白并避让设计较差的key。


另外,主动或者随压力被动删除的计时器(已实现),无卡顿扩容(rehash),内存池内存归还,都可以是丰富附带功能,在此不再描述。


需要源码请致divfor@gmail.com


最后

以上就是忧郁人生为你收集整理的一个高性能无锁哈希表的实现的全部内容,希望文章能够帮你解决一个高性能无锁哈希表的实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部