folly::AtomicHashmap源码分析(一)
本文为原创,转载请注明:http://www.cnblogs.com/gistao/
Atomic的两点背景
看下这个场景,老张去厕所,发现门是锁着的,他就在门口等着里边人出来,此时小王也来了,他想了想,决定去楼上的厕所碰碰运气。
如果把门类比为一种竞争资源的话,老张就像mutex,而小王更像atomic,注意是像而已。
atomic跟传统的通过临界区加锁来避免竞争的多线程处理方式来说,它更像是一种状态机编程,根据当前的状态做出相应的逻辑。
而至于是小王还是老张谁先解决内急,无从得知,同样,mutex vs atomic 的性能对比,实测见真知。
atomic支持的数据类型有限,其他信息可以查看之前我的一篇blog:躲不开的多线程。
Atomic是否适合hashmap
hashmap的数据存储一般是array,每个元素按照自己的index(下标)存放,数据结构天然决定了非常适合lock-free(atomic)。
但hashmap有两个'讨厌'的技术点:rehash和probe。
这里的rehash是说当'空间不够时',需要重新申请一块大的内存,并对之前所有的元素重新hash计算,然后确认其新的index,最后
再一次插入的过程。rehash的同时其他操作不能并行,比如查找,试想一下,在高度并发情况下,百万级别元素的rehash操作是多么的糟糕,
当然这与选择atomic无关,并发情况下,最好不要支持rehash功能。
这里probe是说当多个key求数组下标时,冲突不可能完全避免,自然就有了解决冲突算法:probe,probe的效率相应的决定了查找的效率。
在高度并发情况下,开链比线性探查更适合,因为一个桶的冲突不会影响其他桶。当然开链没有线性探查的局部性好,代码也比线性探查复杂,
使用atomic来编写lock-free数据结构显示不是一件容易的事情,所以显然用后者更可控一些。
AtomicHashmap的关键指标
hashmap的关键指标可能是平均查询时间,也可能是较充分的内存利用。但在AtomicHashmap的设计里,我觉得并发执行效率是第一考虑因素,
所以'锁'一定发生在同一个元素操作中。
其他指标应该等同于一般的hashmap。
AtomicHashmap的使用例子
class Counters {
private:
AtomicHashMap<int64_t,int64_t> ahm; public:
explicit Counters(size_t numCounters) : ahm(numCounters) {} void increment(int64_t obj_id) {
auto ret = ahm.insert(make_pair(obj_id, ));
if (!ret.first) {
// obj_id already exists, increment
NoBarrier_AtomicIncrement(&ret.first->second, );
}
} int64_t getValue(int64_t obj_id) {
auto ret = ahm.find(obj_id);
return ret != ahm.end() ? ret->second : ;
} // Serialize the counters without blocking increments
string toString() {
string ret = "{\n";
ret.reserve(ahm.size() * );
for (const auto& e : ahm) {
ret += folly::to<string>(
" [", e.first, ":", NoBarrier_Load(&e.second), "]\n");
}
ret += "}\n";
return ret;
}
};
AtomicHashmap的实现细节
数据组织结构
数据为二层组织结构,一级数组存储的是二级数组指针,二级数组为元素的真实存储空间,元素是[key,value]的pair。
类组织结构
AtomicHashMap类主要负责对AtomicHashArray对象的创建和管理,以及接口的封装。
AtomicHashArray是hashmap的实现类:插入、查找、删除
空间初始化
当首次插入时
//容量为size和加载因子的除数
size_t capacity = size_t(maxSize / maxLoadFactor);
size_t sz = sizeof(AtomicHashArray) + sizeof(value_type) * capacity; auto const mem = Allocator().allocate(sz);
//将对象指针绑在mem上
new (mem) AtomicHashArray(capacity, c.emptyKey, c.lockedKey, c.erasedKey,
c.maxLoadFactor, c.entryCountThreadCacheSize); //key全部初始化为empty
FOR_EACH_RANGE(i, , map->capacity_) {
cellKeyPtr(map->cells_[i])->store(map->kEmptyKey_,
std::memory_order_relaxed);
}
当插满时
//根据增长因子确认要分配的大小,要分配的大小<*2
size_t numCellsAllocated = (size_t)
(primarySubMap->capacity_ *
std::pow(1.0 + kGrowthFrac_, nextMapIdx - ));
size_t newSize = (int) (numCellsAllocated * kGrowthFrac_);
//初始化一个新submap
Config config;
config.emptyKey = primarySubMap->kEmptyKey_;
config.lockedKey = primarySubMap->kLockedKey_;
config.erasedKey = primarySubMap->kErasedKey_;
config.maxLoadFactor = primarySubMap->maxLoadFactor();
config.entryCountThreadCacheSize =
primarySubMap->getEntryCountThreadCacheSize();
subMaps_[nextMapIdx].store(SubMap::create(newSize, config).release(),
std::memory_order_relaxed);
插入算法
AtomicHashArray的插入算法
insertInternal(KeyT key_in, T&& value) {
//hash + %size ,获取下标
size_t idx = keyToAnchorIdx(key_in);
size_t numProbes = ;
for (;;) {
value_type* cell = &cells_[idx];
//判断cell是否被使用
if (relaxedLoadKey(*cell) == kEmptyKey_) { if (isFull_.load(std::memory_order_acquire)) {
//已经满了,就不能插入了
//返回capacity_,告诉AtomicHashMap类:我这个submap不能再插入了
return SimpleRetT(capacity_, false);
} else {
//还没有满
if (tryLockCell(cell)) {
//tryLockCell其实是compare_exchange_strong,即当这个cell的key为empty时
//将empty状态修改为lock状态
new (&cell->second) ValueT(std::forward<T>(value));
//将cell的key字段标记为插入的key
unlockCell(cell, key_in); // Sets the new key
//已经插入的元素>=最大元素时,标记isFull_为true
//最大元素maxEntries_ == 初始化时的参数size
if (numEntries_.readFast() >= maxEntries_) {
isFull_.store(NO_NEW_INSERTS, std::memory_order_relaxed);
}
//插入成功
return SimpleRetT(idx, true);
}
//注意:线程走到这里,说明之前没有成功trylockcell
//线程继续往下走
}
}
if (kLockedKey_ == acquireLoadKey(*cell)) {
//cell还在被锁定,说明其他线程还在插入这个cell
//等待其他线程插入完成
//为什么要等待,因为其他线程插入可能会失败,也可能其他线程和本身线程插入的key一模一样,
//需要特定的逻辑
FOLLY_SPIN_WAIT(
kLockedKey_ == acquireLoadKey(*cell)
);
} const KeyT thisKey = acquireLoadKey(*cell);
if (EqualFcn()(thisKey, key_in)) {
//比较两个key一样,那本次插入失败,因为之前已经有一个成功插入了
return SimpleRetT(idx, false);
} else if (thisKey == kEmptyKey_ || thisKey == kLockedKey_) {
//两个key不一样,状态又不是插入成功的状态,那么continue了
continue;
} ++numProbes;
if (UNLIKELY(numProbes >= capacity_)) {
//所有的元素空间都遍历一遍了,还是没成功插入,只有失败了
return SimpleRetT(capacity_, false);
}
//线性探测
idx = probeNext(idx, numProbes);
}
}
当AtomicHashArray插入失败,AtomicHashmap会创建一个新的submap,继续插入,周而复始,直至失败。
查找算法
AtomicHashArray的查找算法
findInternal(const KeyT key_in) {
//查找失败,那么遍历查找
for (size_t idx = keyToAnchorIdx(key_in), numProbes = ;
;
idx = probeNext(idx, numProbes)) {
//根据下标,获取key,如果一致,那么查找成功
const KeyT key = acquireLoadKey(cells_[idx]);
if (LIKELY(EqualFcn()(key, key_in))) {
return SimpleRetT(idx, true);
}
//key是个空,说明之前并没有插入过
if (UNLIKELY(key == kEmptyKey_)) {
return SimpleRetT(capacity_, false);
}
++numProbes;
//最坏的情况,全部遍历了一遍,还是没查找,所以失败了
if (UNLIKELY(numProbes >= capacity_)) {
return SimpleRetT(capacity_, false);
}
}
}
这里查找有三个关键点
- 要查的key发生过冲突,那么查找最好的情况是往后遍历一个或者几个找到元素或者发现空元素,然后结束查找,而最坏的情况是遍历查找一遍,当然这种可能性很小,取决于填充因子
- 要查的key并没有插入过,那么最好的情况是遍历到空元素结束,最坏的情况是遍历查找一遍
- 要查的key已经被删除了,这种情况同上
当AtomicHashArray查找失败,AtomicHashmap会寻找下一个被初始化过的submap进行查找,周而复始,直至失败。
删除算法
AtomicHashArray的删除算法
erase(KeyT key_in) {
//遍历删除
for (size_t idx = keyToAnchorIdx(key_in), numProbes = ;
;
idx = probeNext(idx, numProbes)) {
//获取下标的key
value_type* cell = &cells_[idx];
KeyT currentKey = acquireLoadKey(*cell);
//如果key是空的,说明不存在
//如果key正在被插入,那么也是不存在的
if (currentKey == kEmptyKey_ || currentKey == kLockedKey_) {
return ;
} if (EqualFcn()(currentKey, key_in)) {
//找到了元素
KeyT expect = currentKey;
if (cellKeyPtr(*cell)->compare_exchange_strong(expect, kErasedKey_)) {
numErases_.fetch_add(, std::memory_order_relaxed); //将key标记为erase
//为什么没有释放内存,为什么不再把key标记为empty
//因为查找的时候,是把value的指针给的用户,没有时机做那些
return ;
}
//别的线程已经搞定了删除,那么本线程返回就可以了
//返回0?应该返回1
return ;
}
//key不一样,说明之前冲突过
//那么遍历往下找吧
++numProbes;
if (UNLIKELY(numProbes >= capacity_)) {
return ;
}
}
}
当AtomicHashArray删除失败,AtomicHashmap会寻找下一个被初始化过的submap进行删除,周而复始,直至失败
AtomicHashmap的限制
- key只支持32和64位
- 当插入元素数量超过初始化的大小后,性能呈线性下降
- 元素被删除后,空间并没有释放,并且不能再次使用
folly::AtomicHashmap源码分析(一)的更多相关文章
- folly::AtomicHashmap源码分析(二)
本文为原创,转载请注明:http://www.cnblogs.com/gistao/ 背景 上一篇只是细致的把源码分析了一遍,而源码背后的设计思想并没有写,设计思想往往是最重要的,没有它,基本无法做整 ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
随机推荐
- C++ 知道虚函数表的存在
今天翻看陈皓大大的博客,直接找关于C++的东东,看到了虚函数表的内容,找一些能看得懂的地方记下笔记. 0 引子 类中存在虚函数,就会存在虚函数表,在vs2015的实现中,它存在于类的头部. 假设有如下 ...
- 计算机网络(10)-----TCP的拥塞控制
TCP的拥塞控制 拥塞(congestion) 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏. 拥塞控制 拥塞控制就是防止过多的数据注入到网络中,这样可以使网 ...
- 翻转和翻页效果TextFile的几种自定义例子
前一篇文章,已经介绍了BMR的基础用法,再结合Spark和Scala的文档,我想应该是可以开始你的数据分析之路的.这一篇文章,着重进行一些简单的思路上的引导和分析.如果你分析招聘数据时,卡在了某个环节 ...
- (转)Oracle 在Drop表时的Cascade Constraints
Oracle 在Drop表时的Cascade Constraints 假设A为主表(既含有某一主键的表),B为从表(即引用了A的主键作为外键).则当删除A表时,如不特殊说明,则 drop table ...
- 处理xml c#
using System.Xml; using System; using System.Text; using System.Net; using System.Collections; using ...
- Oracle存储过程中异常Exception的捕捉和处理
Oracle存储过程中异常的捕捉和处理 CREATE OR REPLACE Procedure Proc_error_process ( v_IN in Varchar2, v_OUT Out Var ...
- Python开发入门与实战19-Windows Azure web 应用部署
19. 微软云web应用部署 上一章节我们介绍了如何实现在微软云通过虚拟机部署我们的在python django应用,本章我们来介绍如何Windows Azure上部署通过部署网站的方式来部署我们的应 ...
- 初探NIOS II之hello_world
平台背景: 操作系统:win7 64bit 开发板:DE2-115 Quartus ii:15.0及配套的NIOS ii开发平台 一.硬件系统的建立 1.在Quartus里新建工程,这是很基本的就不 ...
- Apache Tomcat开机后台启动
作为软件开发人员,经常接触Tomcat,完成的项目,需要部署到服务器上的Tomcat,才能供其他人访问浏览. 因为存在以下问题,所以需要把Tomcat设置为后台自动启动: 1.服务器可能因环境故障面临 ...
- java filechannel大文件的读写
java读取大文件 超大文件的几种方法 转自:http://wgslucky.blog.163.com/blog/static/97562532201332324639689/ java 读取一个 ...