- 积分
- 10896
- 明经币
- 个
- 注册时间
- 2015-8-18
- 在线时间
- 小时
- 威望
-
- 金钱
- 个
- 贡献
-
- 激情
-
|
本帖最后由 你有种再说一遍 于 2024-6-25 05:08 编辑
继上次教大家怎么实现内存布局之后,这次要说一下多线程了,acad数据库虽然是单线程的,但是大部分人好像当圣经一样遵守了...
懒得干和不会干,是两码事哦...
你不会多线程,那和敲lisp的有什么区别呢...
很多人一开始就觉得,为什么不单线程先读取,再构建集合,然后多线程处理数据,答案也很简单,因为嫌慢.
我首先想到用数组类比数据库,什么时候数组不能多线程读取了?只要不改它就是静态资源,肯定是能读的,所以就是读读不冲突,读写冲突,因此我们需要处理脏写和数组扩容问题.
写入的时候,要先阻塞每个读取线程的"下次读",再等待全部读取任务完成,最后执行写入,写入完成再放开读的阻塞...这不妥妥读写锁嘛!只是这样锁的粒度偏大,但是还是非常好用的.毕竟我们不是频插数据,不然还有跳表,B树,B+树,BLink树,BW树...
因为acad有提供句柄总数,所以并行遍历句柄,我们就能提取图元图层等信息.句柄转id的时候,直接id.Open()是可以的,这并不会引起报错,注意,一定不能打开事务.
利用多线程读取面积等于n的多段线,这不爽翻了吗?联想一下我上一篇批量BO边界...
并行分区
c#阻塞类:AutoResetEvent
c#并行类:Parallel.For
c#linq调用AsParallel会实现自动分区,但是前提需要一个集合.而我们的句柄是0...n只能通过多线程或者Parallel.For乱序执行任务,或者ThreadPool线程池,手动分配.
因为句柄总数是巨大的,读线程需要一个游标指示当前读位置(索引i),被写入线程插入之后,能够继续读后面的,而不是重头开始,但是很快你会发现漏读问题.
漏读
因为句柄分配机制不像动态数组一样是末尾自增,它是一种符号表区间非连续的自增.
那么能获取到每个符号表句柄区间范围吗?不能,但是能通过有效范围进行分析?这可能是另一个话题了,还可以自己实现多线程事务...而且cad官方貌似没有处理句柄溢出情况,一旦溢出就会提示你需要修复图纸.
例如读取线程已经读取[0-100],但是只分配到句柄50,写入记录时候就是句柄51了.这样就产生了,你刚刚添加一个面积等于n的多段线,它就没有读到然后漏存.
怎么办怎么办?
答案是,acad提供了数据库事件啊,嘿嘿.我们只需要利用Database图元添加事件/删除事件/撤回事件,在主线程来维护.
重复读
读取游标在句柄50,那么创建是句柄51,主线程读入51,放行读线程也读入51,这需要hashset句柄咯.
写入
写入刚开始就说了,只是需要改变一下阻塞标记,等待信号量放行.然后可以普通地提交.
事务总是主线程写入,它将包裹多条记录一起提交,然后利用事务可以实现回退.写lisp的用户总是喜欢设置undo标记,它本质只是一次性回滚n次提交.
脏写
因为存在:把多段线修改成面积等于n的时候,这个时候我们就在主线程订阅数据库图元修改事件.
在ifox有一个id.Isok方法来判断id是不是能用的,但是它只能用于主线程:
return id is { IsNull: false, IsValid: true, IsErased: false, IsEffectivelyErased: false, IsResident: true}
而在多线程过滤需要移除:IsValid,IsResident.
句柄文章:https://www.cnblogs.com/JJBox/p/12489648.html
(这样就完成了一个粗糙的多线程dwg数据库读写了)
锁的颗粒度
1,锁定每一间房子,每次只允许一个人进去看书.
2,锁定每一本书,每次只允许一个人进去看书.
你会天然觉得2就是效率更高的,因为房间可以一下子充满人.没错,这就是锁的本质,在不少编程语言都有ConcurentHashmap,ConcurentSet这样里面放锁的类型,而且是原子锁CAS,直接利用硬件锁,拉满性能.
而cad要怎么细化这些锁呢?
下次再告诉你.
读写分离
当你读完上面操作,你会发现,感觉事务是写的时候才需要开启的?
没错,因为读写分离能够避免写锁竞争.
很多人又觉得: cad里面开事务读取好方便啊.那是因为你被cad带坏了,因为cad没有并发,令你长期代入一种"自以为是"的感觉.现在我把并发引入进来了,然后是不是感觉效率提高了?
但是我还是感觉有时候需要读取开事务呢?
那是因为你还没有把"自己很菜"加入进来一起思考,举个例子,如果你能不生成文字直接计算文字包围盒,那么是不是就读写分离了?
当然了,我并不知道生成一个和cad渲染一样的包围盒,它没有提供API,万一它又想提供了呢?万一提供的又和显示不一样呢?所以这只是取舍,这不是原理,因为你做不到而不去思考原理,这才是最笨的,你要自己去做一个cad呢?
什么叫必须?
要告诉你操作系统其实不是必须的,那你肯定要说操作系统真好用...单片机还经常跑裸机呢,我们需要底层原理才能优化,OK?你还在写业务,我已经在考虑性能了,OK?
深入一步,去了解一下其他的数据库.
单线程的数据库代表那就是redis了,它的事务压根没有提供回滚,所以根本没有人用它事务.
那么说明回滚也不是必须的,而原子性却是必须的,没错它们概念分离,虽然事务常常包裹原子性.
redis的修改: 先取再判再改,错误回改.相当于手动模拟了一个回滚,而是这必须是原子性才行lua实现,取和改之间不允许插入其他线程任务,而且有时候需要同时修改几个key.错误总是在编码阶段发现,因此debug时候才需要的.
它的好处是什么?它具备更少信息熵更高的性能.而且如果引入MVCC需要缓存事务消息,并不能节约内存.
多线程才需要考虑事务,参考InnoDB的MVCC,
事务1修改了a=1,还在忙.....
事务2修改了a=2,提交事务2,触发约束冲突,事务回滚.
事务1提交.
选择相似
acad高版本有选择相似命令SelectsLmilar,那么怎么在低版本制造一个相似选择...并组块全图替换.
按图元类型名称索引:
1,初始化,通过并行化乱序读入cad句柄,然后类型名作为key存入ConcurentDictionary.
2,通过Database图元添加事件/删除事件/撤回事件,在主线程来维护索引.
3,索引value需要排序树(不按照包围盒排序,因为四叉树单独做)
用户是选择多个相似的图元和图块,通过获取选区每个图元包围盒求交并比就能提取范围了.然后获取相似堆索引中的图元,求出交并比组块和替换.
因此我们需要一个结构记录相似度,以及相似堆.
struct{
点集数 pts: Points.Lenth
相似堆 ids: List<objectid>
对比数组 distance: float[] 使用距离数组,处理信息熵冗余前缀表?凸度呢
相似度:int 123,-123//镜像相似度*-1.
}
在构建索引时候,遍历一次数据库,加入多段线先判断点集数,无相同点集数就先独立一个加入,在第一次比较时才创建对比数组.
创建对比数组: (1/头尾点距离)*每个前后距离=归一化距离,强转为float.因为溢出也是相同溢出错误,所以能够满足对比.
加入堆:容差比较距离数组,一样的加入同一个堆.
因为存在点序不同,以及环形点序,满足全部要计数.
获取a线第一个距离位于比较数组第几个位置,可能是多个距离相同n,nn,nnn....
n相同时,比较n+1,n+1不等,就下一个nn....循环...直到n全部不满足时候,反转数组再比较n和n+1.
如果n+1相等,n+2不等,那么还是滑动到下一个nn...循环...
为什么是距离数组?
因为距离数组不受逆向影响,只受同向点序影响.
如果是角度数组:每段和x轴的夹角,夹角还可以直接加头尾旋转角用来快速计算.但是角度数组受点序影响很大.例如多段线模拟圆,点序不同时候要线性滑动对比,这个最浪费时间...w左起或者右起,夹角是0-360°难以通过正负或者同方向顺逆时针对比,只能比较两次.w被旋转了呢?所以是错误做法.
Q&A:
自定义图元是僵尸实体?
僵尸实体自己一个集合.
块中
由于句柄遍历的方式是不用考虑嵌套的,我觉得进入块内替换也是很不错,可以提供关键字让用户决定是否替换.同样的,如果选择相似夹着图层锁定,也需要弹提示.
词典中,它可以保存虚拟图元,此时索引上面是存在它的,那么功能也应该过滤掉它.
如何防止没有别人没有插件修改图元造成索引失效?直接多线程重构索引,不用序列化保存索引啦.
DBPoint/XLine/Ray只有一个点数据,岂不是1位置具备大量图元,存在数据倾斜?
由于在不同的类型下实现排序方式不同,此类可以根据点排序,所以他们只是类型下聚合.
以上功能都即将在ifox上面提供,但是鉴于本人失业了,而且电脑已经损坏,遥遥无期,能看懂的自己敲敲吧.
|
|