这篇文章是以前那篇文章iOS管理对象内存的数据结构以及操做算法--SideTables、RefcountMap、weak_table_t的补充和延伸。若是没有阅读过前一篇文章请先看那一篇。
上一篇文章中关于SideTables、SideTable和RefcountMap三者关系可能描述的不太清楚。不少朋友表示看起来晕乎乎的。当初我在研究的时候也是蒙圈了好长一段时间。因此特地写了这篇文章来补充说明一下,同时也有新的知识扩展。
刚开始写文章,思路不太清晰。若是文章中有什么错误或者问题,欢迎你们指正。
这里特别感谢大墙66370午夜时分挑灯夜战提出的宝贵问题。javascript
本文流程
1、解释分离锁是什么。
2、举例阐述SideTables、SideTable、RefcountMap三者关系。
3、第一篇文章所说的N路并发是什么意思。
4、SideTables所使用的Hash算法解密。
5、RefcountMap是什么结构。html
分离锁并非一种锁,而是一种对锁的用法。(下面继续上一张感人的手绘图。哈哈我写的字也出现了。若是比写字丑,通常不多有人能比我写的丑)
java
图1这样对一整个表加一把锁,是咱们平时比较常见的。若是我一次写操做须要操做表中多个单元格的数据,好比第一次操做0、一、2位置的数据,第二次操做0、二、3位置的数据。像这种状况锁的粒度就是以整张表为单位的,才能保证数据的安全。
图2这样对表中的各个元素分别加一把锁就是咱们说的分离锁。适用于表中元素相互独立,你对第一个元素作写操做的时候不须要影响到其余元素。
上文中所说的结构就是SideTables这个大的Hash表中每个小单元格(SideTable)都带有一把锁。作写操做的时候(操做对象引用计数)单元格之间相互独立,互相没影响。因此下降了锁的粒度。算法
对比一下图1和图2的并发性。安全
这里注意区分一下并发和并行的区别。数据结构
当有多个线程在操做时,若是系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分红若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式咱们称之为并发(Concurrent).
当系统有一个以上CPU时,则线程的操做有可能非并发.当一个CPU执行一个线程时,另外一个CPU能够执行另外一个线程,两个线程互不抢占CPU资源,能够同时进行,这种方式咱们称之为并行(Parallel)并发
能够看到在单元格之间相互独立的状况下图2的方法效率更高。
看了上面的例子有同窗会有疑问。既然分离锁能够实现高并发,那么为何不对每个内存对象加一把锁呢?为何还会有Hash表还会冲突呢?这个问题我在下面经过一个例子和RefcountMap一块儿解释。ide
##2、为何SideTables会冲突、SideTable又扮演着什么角色、RefcountMap是用来干吗的?
下面我用一个不太恰当的例子来讲明问题
假设有80个学生须要我们安排住宿,同时还要保证学生们的财产安全。应该怎么安排?
显然不会给80个学生分别安排80间宿舍,而后给每一个宿舍的大门上加一把锁。那样太浪费资源了锁也挺贵的,太多的宿舍维护起来也很费力气。
咱们通常的作法是把80个学生分配到10间宿舍里,每一个宿舍住8我的。假设宿舍号分别是10一、102 、... 110。而后再给他们分配床位,01号床、02号床等。而后给每一个宿舍配一把锁来保护宿舍内同窗的财产安全。为何不仅给整个宿舍楼上一把锁,每次有人进出的时候都把整个宿舍楼锁上?显然这样会形成宿舍楼大门口阻塞。
OK假如如今有人要找102号宿舍的2号床的人聊天。这我的会怎么作?高并发
SideTables[10202]
找到了102宿舍SideTable
,而后把102的门一锁lock
,在他访问102期间再也不容许其余访客访问102了。(这样只是阻塞了102的8个兄弟的访问,而不会影响整栋宿舍楼的访问)table.refcnts.find(02)
你就能够找到2号床的兄弟了。unlock
,这样其余须要访问102的人就能够继续进来访问了。SideTables == 宿舍楼
SideTable == 宿舍
RefcountMap里存放着具体的床位复制代码
苹果之因此须要创造SideTables的Hash冲突是为了把对象放到宿舍里管理,把锁的粒度缩小到一个宿舍SideTable。RefcountMap的工做是在找到宿舍之后帮助你们找到正确的床位的兄弟。优化
上一篇文章中提到:
由于是使用对象的内存地址当key因此Hash的分部也很平均。假设Hash表有n个元素,则能够将Hash的冲突减小到n分之一,支持n路的并发写操做。
咱们在分配宿舍的时候是先给同窗分配宿舍和床位,而后再给宿舍和床位编号。因此咱们能够很平均的给每一个宿舍分配8我的。
那么若是咱们用对象内存地址当作Hash算法的key,所获得的散列值可能会出现某些宿舍分配了4我的,某些宿舍分配了12我的的状况。这样人员分配就不平均了。若是某一时间段正好这个宿舍的12我的的访问量都特别大,那么访问起来就又会出现阻塞了。而那4人间的宿舍就会闲置,形成了资源的浪费。会不会形成这种资源浪费主要看两点。
一、在数据量足够大的状况下咱们的key值分部是平均的。由于key值是内存地址。从低位0x0000...0000到高位0xffff...ffff分配。而且操做系统的内存管理模块自己也会对内存分配作不少优化。毕业年头长了,内管管理具体的细节我也扯不出来了。赶忙贴一片文章压压惊,有兴趣的同窗能够看操做系统内存管理——分区、页式、段式管理。
二、那么SideTables使用的Hash算法是什么呢?咱们来开一个新的大标题。
SideTables的定义:NSObject.mm line 207-209
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}复制代码
若是看不懂不要紧,当它是一个C++的Map。我们来看StripedMap的定义:objc-private.h line 867-906
其中有用的部分是这样的
...
//若是是嵌入式系统StripeCount=8。咱们这里StripeCount=64
enum { StripeCount = 64 };
...
static unsigned int indexForPointer(const void *p) {
//这里是类型转换,不用在乎
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
//这里就是咱们要找的Hash算法了
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}复制代码
addr
右移4位获得结果1addr
右移9位获得结果2由于最后模运算的结果范围是在0-63之间,可见SideTables一共有64个单元格。
前面文章中提到过if(引用计数器 != table.refcnts.end())
所以有同窗提问end()
是什么?那么我们得研究一下RefcountMap是什么类型的。
看定义NSObject.mm line 137
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;复制代码
看起来好复杂,先无论它。一路顺着定义找下去。找到DenseMap ==> DenseMapBase ==> inline iterator end()
发现我们当前在llvm-DenseMap.h line 77-79。艾玛吓死我了怎么看到llvm了。llvm我也不懂,因此仍是不要扯的太远。赶忙打开llvm的相关文档看看DenseMapBase中的公开方法有
固然还有更多其余方法,我只是截取了一部分。经过这部分咱们能够看出来咱们操做的refcnts.find()和refcnts.end()其实都是对一个C++迭代器iterator的操做。而end()的状态表示的是从头开始查找,一直找到最后都没有找到。当前指针指向的是结束位,而不是最后一个元素。
因此 if(引用计数器 == table.refcnts.end())
表示查找到最后都没找到if(引用计数器 != table.refcnts.end())
表示中途找到了。
讲到这里想讲的内容已经讲完啦,欢迎评论、点赞、转发。
欢迎加个人微博weibo.com/xuyang186
转载请注明出处,谢谢。