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大做完了之后,我发现上面论坛链接的第二页居然有....

img
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);
}
}

缺省函数

AcdbAdsHelper.AcdbEntGet

// 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();
posted @ 2020-03-14 07:13  惊惊  阅读(1508)  评论(0编辑  收藏  举报