cad.net 利用句柄遍历+并行遍历数据库
说明
这是一种利用句柄遍历数据库的方式,句柄是acad内部分配器进行递增的.
在某些情况下(可能是天正环境导致),用此方法遍历数据库奇慢,
会在循环中一直自增,为了停止它,我使用了一个变量.
第二种奇慢是acad08上面,acad程序员把断言和vs输出弄反了,
导致debug模式一直弹出miss信息.
为了遍历可能的千万级句柄,
使用Parallel.For并行遍历转换句柄到id,
能够更快实现对id获取.
20241114补充
在解析DWG的时候发现句柄区,利用它进行并行读取,可以减少大量的miss情况.
以下面的例子都是有miss情况的,我不做过多处理,因为它就不像一个简单例子了.
信息挖掘
ObjectIds上缓慢的迭代
同时因为id = db.GetObjectId(false, handle, 0);
在acad08的表现非常慢,所以我被迫调用了Arx函数.
当我和e大做完了之后,我发现上面论坛链接的第二页居然有....
edata :打开文件后,句柄是2c5,继续画直线:2c6...2c7...2c8
如果此时保存了,就从313开始,重新打开后,句柄浪费了75个左右...
ctrl+c乱增,co就不会乱增...
遍历组码
同时发现这是遍历dwg所有组码的方式,因为entget并不能获取动态块组码信息,所以才需要它.
貌似还可以ent.DxfOut
有了组码,就可以获取动态块可见性名称了,其他的或许也可以...
分析动态块组码见动态块专篇
Arx代码见e大博文遍历动态块块定义的可见性名称方法
代码
#if !HC2020
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
#else
using GrxCAD.DatabaseServices;
using GrxCAD.Runtime;
using GrxCAD.EditorInput;
using Acap = GrxCAD.ApplicationServices.Application;
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace JoinBox.试验库
{
public class CmdTest_FastIterationClass
{
[CommandMethod("CmdTest_FastIteration")]
public void CmdTest_FastIteration()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ed.WriteMessage($"{Environment.NewLine}利用句柄找到动态块可见性的名称");
var dic = db.HandleToObjectId();
int entNum = 0;
using var tr = db.TransactionManager.StartTransaction();
foreach (var dicItem in dic) {
var id = dicItem.Value;
if (!id.IsOk()) continue;
string dxfName = id.GetDxfName();
ed.WriteMessage($"{Environment.NewLine}图元{++entNum}:DxfName={dxfName};句柄={dicItem.Key};Id={id}");
if (dxfName == "BLOCKVISIBILITYPARAMETER")
{
AcdbAdsHelper.AcdbGetAdsName(out AdsName eName, id);
var elist = AcdbAdsHelper.AcdbEntGet(ref eName);
if (elist == IntPtr.Zero) {
using var rb = new ResultBuffer();
elist = AcdbAdsHelper.AcdbEntGetX(ref eName, rb.UnmanagedObject);
}
if (elist == IntPtr.Zero)
continue;
using var rb2 = ResultBuffer.Create(elist, true);
foreach (var tv in rb2) {
if (tv.TypeCode == 303)//动态块的可见性参数被改的名称记录位置
PrintDxf(tv, ed);
}
}
}
ed.WriteMessage($"{Environment.NewLine}结束了!");
}
public void PrintDxf(TypedValue ty, Editor ed)
{
ed.WriteMessage($"{Environment.NewLine}TypeCode:{ty.TypeCode};;;Value:{ty.Value}");
}
}
public static class DatabaseHelper
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetDxfName(this ObjectId id)
{
string dxfName;
#if !NET35
dxfName = id.ObjectClass.DxfName;
#else
// DBObject obj = tr.GetObject(id, OpenMode.ForRead);节约事务参数
// dxfName = RXClass.GetClass(obj.GetType()).DxfName;//这里并不能准确获取到dxfName
using DBObject obj = id.Open(OpenMode.ForRead);
dxfName = obj.GetRXClass().DxfName;
#endif
return dxfName;
}
/// <summary>
/// 遍历句柄获取数据库的ids
/// 可能是已经删除的,不一定满足id.IsOk()
/// </summary>
/// <param name="db"></param>
/// <param name="ed">控制是否打印时间</param>
public static Dictionary<Handle, ObjectId> HandleToObjectId(this Database db, Editor ed = null)
{
Dictionary<Handle, ObjectId> dictResult = new();
DateTime start = DateTime.Now;//时间计数
long time = 0; //时间计数
db.TraverseHandle(h => {
Handle handle = new(h);
var id = db.TryGetObjectId(handle);
if (id != ObjectId.Null)
dictResult.Add(handle, id);
if (ed != null) {
//计时输出
var len = DateTime.Now - start;
if (len.Seconds != time && len.Seconds % 5 == 0)//5秒输出一次
{
time = len.Seconds;
ed.WriteMessage($"{Environment.NewLine}数量: {h},已经过了{len.Days}天,{len.Hours}小时,{len.Minutes}分,{len.Seconds}秒");
ed.UpdateScreenEx();//此函数写在 https://www.cnblogs.com/JJBox/p/11354224.html
}
}
});
return dictResult;
}
/// <summary>
/// 遍历句柄
/// </summary>
/// <param name="db">数据库</param>
/// <param name="action">扔出去个迭代句柄数字</param>
#if !NET35 && !NET40
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
static void TraverseHandle(this Database db, Action<long> action)
{
#if !NET35
// 遍历块表头尾(不是数据库头尾)
// 青蛙说下面这个注释的不能用了,不知道这个偏移量计算方式
// 是不是new-old==0就是新cad开dwg07的偏移量计算呢?又或者是第一个转换id无效,就跳转到第二个?
// var entLast = Autodesk.AutoCAD.Internal.Utils.EntLast().OldIdPtr;
// var entStart = db.BlockTableId.OldIdPtr;
var entLast = Autodesk.AutoCAD.Internal.Utils.EntLast().Handle.Value;
var entStart = db.BlockTableId.Handle.Value;
if (IntPtr.Size == 4) {
var e = entLast.ToInt32();//防止句柄递增
for (var i = entStart.ToInt32(); i < e; i++)//可以改为并行Parallel.For
action(i);
}
else if (IntPtr.Size == 8) {
var e = entLast.ToInt64();//防止句柄递增
for (var i = entStart.ToInt64(); i < e; i++)//可以改为并行Parallel.For
action(i);
}
#else
// 遍历块表头到数据库尾
// (按理说应该0到数据库尾/或者块表头尾,这个版本应该是缺失了块表尾API)
var dbLast = db.Handseed.Value;//防止句柄递增
var entStart = db.BlockTableId.Handle.Value;
for (var i = entStart; i < dbLast; i++)//可以改为并行Parallel.For
action(i);
#endif
}
}
public static class TryGetObjectIdHelper
{
/*
* id = db.GetObjectId(false, handle, 0);
* 参数意义: db.GetObjectId(如果没有找到就创建,句柄号,标记..将来备用)
* 在vs的输出会一直抛出:
* 引发的异常:“Autodesk.AutoCAD.Runtime.Exception”(位于 AcdbMgd.dll 中)
* "eUnknownHandle"
* 这就是为什么慢的原因,所以直接运行就好了!而Debug还是需要用arx的API替代.
*/
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb17.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QAE?AW4ErrorStatus@Acad@@AAVAcDbObjectId@@_NABVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId17x32(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb17.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QEAA?AW4ErrorStatus@Acad@@AEAVAcDbObjectId@@_NAEBVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId17x64(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb18.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QAE?AW4ErrorStatus@Acad@@AAVAcDbObjectId@@_NABVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId18x32(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb18.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QEAA?AW4ErrorStatus@Acad@@AEAVAcDbObjectId@@_NAEBVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId18x64(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
/// <summary>
/// 句柄转id,NET35(08~12)专用的
/// </summary>
/// <param name="db">数据库</param>
/// <param name="handle">句柄</param>
/// <param name="id">返回的id</param>
/// <param name="createIfNotFound">不存在则创建</param>
/// <param name="reserved">保留,用于未来</param>
/// <returns>成功0,其他值都是错误.可以强转ErrorStatus</returns>
#if !NET35 && !NET40
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
static int GetAcDbObjectId(IntPtr db, Handle handle, out ObjectId id, bool createIfNotFound = false, uint reserved = 0)
{
id = ObjectId.Null;
switch (Acap.Version.Major) {
case 17: {
if (IntPtr.Size == 4)
return getAcDbObjectId17x32(db, out id, createIfNotFound, ref handle, reserved);
else
return getAcDbObjectId17x64(db, out id, createIfNotFound, ref handle, reserved);
}
case 18: {
if (IntPtr.Size == 4)
return getAcDbObjectId18x32(db, out id, createIfNotFound, ref handle, reserved);
else
return getAcDbObjectId18x64(db, out id, createIfNotFound, ref handle, reserved);
}
}
return -1;
}
/// <summary>
/// 句柄转id
/// </summary>
/// <param name="db">数据库</param>
/// <param name="handle">句柄</param>
/// <returns>id</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObjectId TryGetObjectId(this Database db, Handle handle)
{
#if !NET35
//高版本直接利用
var es = db.TryGetObjectId(handle, out ObjectId id);
//if (!es)
#else
var es = GetAcDbObjectId(db.UnmanagedObject, handle, out ObjectId id);
//if (ErrorStatus.OK != (ErrorStatus)es)
#endif
return id;
}
/// <summary>
/// 此命令用来测试:内部调用NET35(08~12)专用的 GetAcDbObjectId,
/// 外部调用封装的 TryGetObjectId.
/// </summary>
[CommandMethod("CmdTest_handloop")]
public static void CmdTest_handloop()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
// 遍历数据库头尾,串行例子
for (long i = 0; i < db.Handseed.Value; i++) {
Handle handle = new(i);
var es = GetAcDbObjectId(db.UnmanagedObject, handle, out ObjectId id);
if (ErrorStatus.OK == (ErrorStatus)es)
ed.WriteMessage("\n执行成功");
if (id.IsNull) continue;
if (id.IsValid) ed.WriteMessage($"\n有效ID:{id},句柄是{handle}");
else ed.WriteMessage($"\n无效ID:{id},句柄是{handle}");
if (id.IsErased) {
ed.WriteMessage("是已经删除的ID");
continue;
}
using var tr = db.TransactionManager.StartTransaction();
using DBObject obj = tr.GetObject(id, OpenMode.ForRead);
if (obj == null) continue;
ed.WriteMessage($"\n句柄={handle};DxfName={id.GetDxfName(tr)}");
//九个符号表
if (obj is BlockTable)
ed.WriteMessage("\nID是table");
}
}
}
}
并行例子_统计块数量
本例子关联:
https://www.cnblogs.com/JJBox/p/16002267.html
问题:
1,当执行第2次命令,并行遍历句柄之后会无法获取任何id,所以没有打印信息.
提示: eAtMaxReader
2,无法画入图元.
3,保存出错,提示: 此图形的一个或多个对象无法保存为指定格式.操作未完成,因此未创建任何文件.
因此,我们只能仅一次使用,只能关闭图纸再另外开图?
public class Test{
[CommandMethod("CmdTest_GetBlocks")]
public static void CmdTest_GetBlocks()
{
static bool IsOkEx(ObjectId id){
return !(id.IsNull || id.IsErased || id.IsEffectivelyErased);
}
static string GetBlockName(ObjectId eid) {
var ent = eid.Open(OpenMode.ForRead);
string name = "";
if (ent is BlockReference brf && IsOkEx(brf.DynamicBlockTableRecord)) {
var btr = (BlockTableRecord)brf.DynamicBlockTableRecord.Open(OpenMode.ForRead);
name = btr.Name;
btr.Dispose();
}
ent.Dispose();
return name;
}
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ConcurrentDictionary<string, ConcurrentBag<ObjectId>> map = new();
Parallel.For(0, db.Handseed.Value, handleIndex => {
Handle handle = new(handleIndex);
var id = TryGetObjectIdHelper.TryGetObjectId(db, handle);
if (!IsOkEx(id))
return;
var blockName = GetBlockName(id);
if (blockName == "") return;
// 统计同块数量
map.AddOrUpdate(blockName,
new ConcurrentBag<ObjectId> { id },
(key, existingBag) => {
existingBag.Add(id);
return existingBag;
});
});
// 第二次时候会没有内容
foreach(var group in map){
Env.Editor.WriteMessage($"\n{group.Key}: {group.Value.Count()}");
}
}
}
并行遍历_归类测试,没写完
并行遍历数据库头尾,统计块的引用数量.
并行就不要开事务,用Open只读.
归类到线程安全字典,从而制造数据库索引.
不同的句柄get的id可能是重复的.
OpenMode.ForRead限制256个只读器,似乎超过也没有问题,不明白为什么写这个出来...
net8的有序并发容器其他net版本可以拷贝这个代码到自己的工程上面.或者采用java的跳表/ConcurrentHashSet之类的,否则下面无法消重
// 分类索引的别名<dxfName,图元集合>集合选择跳表或者其他线程安全排序容器(排序为了消重,不同句柄可能指向相同id)
using EntityMapType = ConcurrentDictionary<string,ConcurrentHashSet<ObjectId>>();
public class Test {
static EntityMapType EntityMap = [];
static ConcurrentQueue<(Database, long)> queue = new();
static long atomicCount = 0;
static LoopState state = new(); //并行中断标记
[CommandMethod("CmdTest_handloop")]
public static void CmdTest_handloop()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
Stopwatch sw = new Stopwatch();
sw.Restart();
// 消费者线程,由CPU核心数确定数量
for (int i = 0; i < 16; i++)
new Thread(CounterThread).Start();
// 生产者线程,不限制可以直接DoWork()
Parallel.For(0, db.Handseed.Value, handleIndex => {
queue.Enqueue((db, handleIndex));
});
/* 并行会阻塞,我们组建索引只需要打开图纸的时候让异步组建,使用再问state完成没,不需要并行阻塞.但是要解决跨线程获取数据库问题.
for(int i = 0;i < 16; i++)
new Thread(CounterThread2).Start();
*/
ed.WriteMessage($"耗时={sw.ElaspseString()}");
}
// 消费者线程,从队列中取出数据
static void CounterThread()
{
while (state.IsRun) {
long atomicValue = Interlocked.Read(ref atomicCount); // 多个线程会取到相同值
if (atomicValue < 256L // 限制只读器
&& queue.TryDequeue(out (Database, long) result)) {
Interlocked.Increment(ref atomicCount);
DoWork(result.Item1, result.Item2);
Interlocked.Decrement(ref atomicCount);
continue;
}
Thread.Sleep(1);
}
}
static void DoWork(Database db, long handleIndex)
{
Handle handle = new(handleIndex);
var es = (ErrorStatus)TryGetObjectIdHelper.getAcDbObjectId19x64(db.UnmanagedObject, out ObjectId id, false, ref handle, 0);
if (ErrorStatus.OK != es || id.IsNull || id.IsErased || id.IsEffectivelyErased)
return;
// 如果要打开id,那么要采取open/close方法,或者using.
using DBObject obj = id.Open(OpenMode.ForRead);
if(obj is Entity ent){...}
var dxfName = id.ObjectClass.Name;
EntityMap.AddOrUpdate(dxfName,new ConcurrentBag<ObjectId>(){id},(key,bag)=> { bag.Add((id));return bag;});
}
// 写数据库开启完成的事件上面,开启n个线程,全部任务完成会设置state,不使用并行阻塞.
static void CounterThread2()
{
/* 多线程内部get database会报错,为了避免此问题,将用数据库启动事件代替
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
var seed = db.Handseed.Value;
这里仅仅是测试,为了性能应该直接:
var value = Interlocked.Increment(ref atomicCount)
而不是像下面一样使用CAS,大量比较失败会变卡
*/
while (state.IsRun) {
var value = atomicCount; // 多个线程可能重复记录value,如果替换不成功跳转,否则用原值进行下一步.
if (Interlocked.CompareExchange(ref value, value + 1, atomicCount) != atomicCount)
continue;
if (value < seed) DoWork(db, value);
else state.Stop();
}
}
}//class
数据库启动事件...伪代码
// 分类索引的别名<dxfName,图元集合>集合选择跳表或者其他线程安全排序容器(排序为了消重,不同句柄可能指向相同id)
using EntityMapType = ConcurrentDictionary<string,ConcurrentHashSet<ObjectId>>();
class DatabaseOpenTest2 {
// 这里全局变量不要用Databse因为导致无法释放,所以用databaseFileName,
// 线程上下文问题在事件内穿透传值.
static Dictionary<string,(LoopState state,Task task)> IndexTask=[];
static Dictionary<string,EntityMapType> IndexMap=[];
public void Test() {
// 事件这里是单线程
// https://help.autodesk.com/view/OARX/2022/ENU/?guid=GUID-925C090F-C90F-4F1E-91F7-E39FF48E5655
editor.EndDwgOpen+=(s,e)=>{
var db = e.Databse;
var seed = db.Handseed.Value;
// 下面这两个对象生命周期会被字典转移到全局
var state = new LoopState();
EntityMapType entityMap=[];
// Task包装实现非阻塞并行,内部是线程不安全部分
var task = Task.Run(() => {
Parallel.For(0, seed/*穿透线程*/, i => {
// CPU密集型任务,构造索引...
DoWork(db,i);
entityMap.Add(dxfName,图元....);
});
state.Stop();
});
IndexTask.Add(db.FileName,(state,task));
IndexMap.Add(db.FileName,entityMap);
task.Start();
});
}
// 通过数据库名,获取分类索引.
public bool TryGetValue<T,V>(string dbFile, T key, out V value){
// 利用线程标记阻塞获取(什么情况可以非阻塞获取?好像没有,不如删掉状态,直接调用wait算了)
var d = IndexTask[dbFile];
if(d.state.IsStop()) d.task.Wait();
return IndexMap[dbFile].TryGetValue(key,out value);
}
}
缺省函数
// https://gitee.com/inspirefunction/ifoxcad/blob/v0.7/src/Basal/IFox.Basal.Shared/General/LoopState.cs
/// <summary>
/// 控制循环结束
/// </summary>
public class LoopState {
private const int PlsNone = 0;
private const int PlsExceptional = 1;
private const int PlsBroken = 2;
private const int PlsStopped = 4;
private const int PlsCanceled = 8;
private volatile int _flag = PlsNone;
public bool IsRun => _flag == PlsNone;
public bool IsExceptional => (_flag & PlsExceptional) == PlsExceptional;
public bool IsBreak => (_flag & PlsBroken) == PlsBroken;
public bool IsStop => (_flag & PlsStopped) == PlsStopped;
public bool IsCancel => (_flag & PlsCanceled) == PlsCanceled;
public void Exceptional() {
if ((_flag & PlsExceptional) != PlsExceptional)
_flag |= PlsExceptional;
}
public void Break() => _flag = PlsBroken;
public void Stop() => _flag = PlsStopped;
public void Cancel() => _flag = PlsCanceled;
public void Reset() => _flag = PlsNone;
}
替换句柄
https://www.cnblogs.com/d1742647821/p/18310874
要改用:
StartOpenCloseTransaction
var doc = Acap.DocumentManager.MdiActiveDocument;
var r1 = doc.Editor.GetEntity("\n选择要被替换的对象");
if (r1.Status != PromptStatus.OK)
return;
using var tr = doc.Database.TransactionManager.StartOpenCloseTransaction();
var selectEntity = (Entity)tr.GetObject(r1.ObjectId, OpenMode.ForWrite);
var newEntity = new Line(Point3d.Origin, new Point3d(100, 100, 0));
selectEntity.HandOverTo(newEntity, true, true);
tr.AddNewlyCreatedDBObject(newEntity, true);
tr.Commit();