这篇文章是之前那篇文章iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t的补充和延伸。如果没有阅读过前一篇文章请先看那一篇。
上一篇文章中关于SideTables、SideTable和RefcountMap三者关系可能描述的不太清楚。很多朋友表示看起来晕乎乎的。当初我在研究的时候也是蒙圈了好长一段时间。所以特意写了这篇文章来补充说明一下,同时也有新的知识扩展。
刚开始写文章,思路不太清晰。如果文章中有什么错误或者问题,欢迎大家指正。
这里特别感谢大墙66370午夜时分挑灯夜战提出的宝贵问题。
本文流程
一、解释分离锁是什么。
二、举例阐述SideTables、SideTable、RefcountMap三者关系。
三、第一篇文章所说的N路并发是什么意思。
四、SideTables所使用的Hash算法解密。
五、RefcountMap是什么结构。
一、分离锁
分离锁并不是一种锁,而是一种对锁的用法。(下面继续上一张感人的手绘图。哈哈我写的字也出现了。如果比写字丑,一般很少有人能比我写的丑)
图1这样对一整个表加一把锁,是我们平时比较常见的。如果我一次写操作需要操作表中多个单元格的数据,比如第一次操作0、1、2位置的数据,第二次操作0、2、3位置的数据。像这种情况锁的粒度就是以整张表为单位的,才能保证数据的安全。
图2这样对表中的各个元素分别加一把锁就是我们说的分离锁。适用于表中元素相互独立,你对第一个元素做写操作的时候不需要影响到其他元素。
上文中所说的结构就是SideTables这个大的Hash表中每一个小单元格(SideTable)都带有一把锁。做写操作的时候(操作对象引用计数)单元格之间相互独立,互相没影响。所以降低了锁的粒度。
对比一下图1和图2的并发性。
- 图1因为任何操作都需要锁整张表,所以写操作的时候相当于串行操作。没有并发。
- 图2因为每一个单元格都有一把锁,所以写操作的时候有多少个单元格并发数就可以是多少。
这里注意区分一下并发和并行的区别。
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent).
当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)
可以看到在单元格之间相互独立的情况下图2的方法效率更高。
看了上面的例子有同学会有疑问。既然分离锁可以实现高并发,那么为什么不对每一个内存对象加一把锁呢?为什么还会有Hash表还会冲突呢?这个问题我在下面通过一个例子和RefcountMap一起解释。
二、为什么SideTables会冲突、SideTable又扮演着什么角色、RefcountMap是用来干嘛的?
下面我用一个不太恰当的例子来说明问题
假设有80个学生需要咱们安排住宿,同时还要保证学生们的财产安全。应该怎么安排?
显然不会给80个学生分别安排80间宿舍,然后给每个宿舍的大门上加一把锁。那样太浪费资源了锁也挺贵的,太多的宿舍维护起来也很费力气。
我们一般的做法是把80个学生分配到10间宿舍里,每个宿舍住8个人。假设宿舍号分别是101、102 、... 110。然后再给他们分配床位,01号床、02号床等。然后给每个宿舍配一把锁来保护宿舍内同学的财产安全。为什么不只给整个宿舍楼上一把锁,每次有人进出的时候都把整个宿舍楼锁上?显然这样会造成宿舍楼大门口阻塞。
OK假如现在有人要找102号宿舍的2号床的人聊天。这个人会怎么做?
- 1、找到宿舍楼(SideTables)的宿管,跟他说自己要找10202(内存地址当做key)。
- 2、宿管带着他
SideTables[10202]
找到了102宿舍SideTable
,然后把102的门一锁lock
,在他访问102期间不再允许其他访客访问102了。(这样只是阻塞了102的8个兄弟的访问,而不会影响整栋宿舍楼的访问) - 3、然后在宿舍里大喊一声:"2号床的兄弟在哪里?"
table.refcnts.find(02)
你就可以找到2号床的兄弟了。 - 4、等这个访客离开的时候会把房门的锁打开
unlock
,这样其他需要访问102的人就可以继续进来访问了。
SideTables == 宿舍楼
SideTable == 宿舍
RefcountMap里存放着具体的床位
苹果之所以需要创造SideTables的Hash冲突是为了把对象放到宿舍里管理,把锁的粒度缩小到一个宿舍SideTable。RefcountMap的工作是在找到宿舍以后帮助大家找到正确的床位的兄弟。
三、N路的并发写操作那句话是什么意思?
上一篇文章中提到:
因为是使用对象的内存地址当key所以Hash的分部也很平均。假设Hash表有n个元素,则可以将Hash的冲突减少到n分之一,支持n路的并发写操作。
我们在分配宿舍的时候是先给同学分配宿舍和床位,然后再给宿舍和床位编号。所以我们可以很平均的给每个宿舍分配8个人。
那么如果我们用对象内存地址当做Hash算法的key,所得到的散列值可能会出现某些宿舍分配了4个人,某些宿舍分配了12个人的情况。这样人员分配就不平均了。如果某一时间段正好这个宿舍的12个人的访问量都特别大,那么访问起来就又会出现阻塞了。而那4人间的宿舍就会闲置,造成了资源的浪费。会不会造成这种资源浪费主要看两点。
1、我们的key值分部是否平均。
-
2、我们采用的散列算法能不能尽量把输出值平均分配。
1、在数据量足够大的情况下我们的key值分部是平均的。因为key值是内存地址。从低位0x0000...0000到高位0xffff...ffff分配。并且操作系统的内存管理模块本身也会对内存分配做很多优化。毕业年头长了,内管管理具体的细节我也扯不出来了。赶紧贴一片文章压压惊,有兴趣的同学可以看操作系统内存管理——分区、页式、段式管理。
2、那么SideTables使用的Hash算法是什么呢?我们来开一个新的大标题。
四、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;
}
1、将对象的内存地址
addr
右移4位得到结果12、将对象的内存地址
addr
右移9位得到结果23、将结果1和结果2做按位异或得到结果3
-
4、将结果3和StripeCount做模运算得到真正的Hash值。
因为最后模运算的结果范围是在0-63之间,可见SideTables一共有64个单元格。
五、RefcountMap是什么结构。
前面文章中提到过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())
表示中途找到了。
讲到这里想讲的内容已经讲完啦,欢迎评论、点赞、转发。
欢迎加我的微博http://weibo.com/xuyang186
** 转载请注明出处,谢谢。**