================================================================================
「后台任务随时取消」完整技术方案(原理 + 函数 + 新手步骤 + 完整代码)
适用场景:AutoCAD C# 二次开发 / .NET 桌面应用 耗时计算取消
================================================================================
版本:v2.0(二合一完整版)
目标读者:从零基础新手到需要深入原理的开发者
阅读时间:完整阅读约 30 分钟,按步骤操作约 15 分钟
═══════════════════════════════════════════════════════════════════════════════
第一部分:核心原理(为什么要这样做)
═══════════════════════════════════════════════════════════════════════════════
1.1 什么是"协作式取消"?
─────────────────────────────────────────────────────────────────────────────
不是强制终止线程(如 Thread.Abort),而是通过一个共享的布尔变量作为"停止旗帜",
让所有正在运行的代码主动"看见"并"退让"。
流程:
用户点击取消
↓
设置取消标志 = true
↓
耗时循环每次迭代都检查该标志
↓
发现标志为 true → 立即 break/return
↓
清理资源 → 退出
优点:安全(不会破坏非托管资源)、可控(在代码安全点退出)、兼容性好。
1.2 为什么不能直接用 Thread.Abort()?
─────────────────────────────────────────────────────────────────────────────
Thread.Abort() 会抛出 ThreadAbortException,强制中断任何代码。
如果中断发生在 using 块、事务(Transaction)或原生 DLL 调用中,
会导致资源泄漏、CAD 崩溃或数据库损坏。
在 AutoCAD 二次开发中,禁止使用 Thread.Abort()。
1.3 为什么需要后台线程 + 定时器?
─────────────────────────────────────────────────────────────────────────────
如果不使用后台线程,耗时计算会阻塞 UI 线程,取消按钮无法响应点击事件。
如果仅使用后台线程但不用定时器,后台线程无法安全地更新 UI 控件(进度条、文字)。
定时器在 UI 线程上触发 Tick 事件,可以安全地检查取消标志并更新 UI。
1.4 为什么需要消息队列(ConcurrentQueue)?
─────────────────────────────────────────────────────────────────────────────
后台线程不能直接调用 ed.WriteMessage()(AutoCAD Editor 不是线程安全的)。
使用 ConcurrentQueue 暂存日志消息,由 UI 定时器在 UI 线程中消费并输出。
ConcurrentQueue 是线程安全的,无需额外加锁。
1.5 volatile 关键字的作用?
─────────────────────────────────────────────────────────────────────────────
volatile 告诉编译器和 CPU:该变量可能被多个线程修改,不要对它做优化缓存。
保证一个线程对变量的修改,能被其他线程立即看到(内存屏障)。
如果不加 volatile,在 Release 编译模式下,循环可能因为 CPU 缓存而永远看不到标志变化。
1.6 新旧项目对比:为什么旧项目秒停,新项目卡住?
─────────────────────────────────────────────────────────────────────────────
旧项目(CadNest V4.7.5):
-
求解器有独立的 Cancel() 方法(可能通过进程间通信终止外部进程)
-
计算任务被拆碎成细粒度并行(Parallel.ForEach),每处理完一个零件就检查取消标志
-
取消响应时间 = 当前零件计算耗时(通常毫秒级)
新项目(DeepNest):
-
深层循环(Background.placeParts)没有取消检查点
-
原生 DLL(minkowski.dll)无法从 C# 层中断
-
取消响应时间 = 当前原生函数调用耗时(可能数秒)
解决方案:
-
在所有托管循环中添加取消检查
-
原生 DLL 无法中断,只能等待返回,但至少在循环间隙能立即响应
═══════════════════════════════════════════════════════════════════════════════
第二部分:用到的关键技术点与函数详解
═══════════════════════════════════════════════════════════════════════════════
2.1 命名空间引用(所有需要添加的 using)
─────────────────────────────────────────────────────────────────────────────
using System;
using System.Threading;
using System.Threading.Tasks; // Task.Run
using System.Collections.Concurrent; // ConcurrentQueue
using System.Windows.Forms; // Timer, Application.DoEvents
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
2.2 核心函数/属性清单
─────────────────────────────────────────────────────────────────────────────
【取消标志】
public static volatile bool CancelRequested;
作用:全局共享的取消标志,所有线程可见。
【进度窗体】
public bool IsCancelled { get; private set; }
作用:用户是否点击了取消按钮。
btnCancel.Click += (s, e) => { this.IsCancelled = true; }
作用:点击取消按钮时设置标志。
【命令入口】
CommandMethod("命令名") public void 命令名()
作用:AutoCAD 命令入口,重置标志并启动异步流程。
【异步编排】
private void RunDirectNestingAsync(...)
作用:启动后台任务和定时器,协调进度更新和收尾。
Task.Run(() => { ... })
作用:在后台线程池中执行耗时计算。
System.Windows.Forms.Timer _pollTimer;
作用:UI 定时器,每 200ms 检查取消状态和任务完成状态。
_pollTimer.Tick += (s, e) => { ... }
作用:定时器触发时的回调函数。
【消息队列】
private static ConcurrentQueue<string> _messageQueue
作用:线程安全的日志消息队列。
_messageQueue.Enqueue("消息");
作用:后台线程入队日志。
_messageQueue.TryDequeue(out string msg)
作用:定时器出队并输出日志。
【跨线程 UI 更新】
progress.Invoke((MethodInvoker)delegate { ... });
作用:在 UI 线程上执行委托,安全更新进度条。
try { ... } catch (ObjectDisposedException) { break; }
作用:捕获窗体已关闭的异常,避免程序崩溃。
【资源清理】
progress.Close();
作用:关闭进度窗体,释放资源。
_pollTimer.Stop(); _pollTimer = null;
作用:停止定时器,允许垃圾回收。
【AutoCAD 文档锁定】
using (DocumentLock docLock = doc.LockDocument())
作用:在操作数据库前锁定文档,防止冲突。
using (Transaction tr = db.TransactionManager.StartTransaction())
作用:事务管理,确保数据库操作原子性。
═══════════════════════════════════════════════════════════════════════════════
第三部分:分步实施指南(新手操作步骤)
═══════════════════════════════════════════════════════════════════════════════
【前置准备】
-
确认你的项目是 .NET Framework 4.8 或更高版本(支持 Task.Run)。
-
确认项目引用了 System.Windows.Forms(用于 Timer 和 ProgressForm)。
-
确认项目引用了 AutoCAD .NET API 库(acdbmgd.dll、acmgd.dll)。
【第 1 步:创建全局取消标志】
打开或新建一个静态类文件(例如 GlobalCancel.cs)。
粘贴以下代码:
---------------------------------------- 复制开始 ----------------------------------------
using System;
namespace DeepNestLib
{
public static class GlobalCancel
{
// ★★★ 核心:volatile 保证多线程可见性 ★★★
public static volatile bool IsRequested = false;
// ★★★ 每次命令开始必须调用 ★★★
public static void Reset() => IsRequested = false;
}
}
---------------------------------------- 复制结束 ----------------------------------------
新手常见错误:忘记加 volatile。如果不加,Release 模式下 CPU 缓存会导致循环永远看不到标志变化。
【第 2 步:修改进度窗体(ProgressForm)】
找到你的 ProgressForm 类,确保取消按钮的 Click 事件包含以下内容:
---------------------------------------- 复制开始 ----------------------------------------
// 在 ProgressForm 类中,必须有一个公共属性
public bool IsCancelled { get; private set; } = false;
// 在取消按钮的 Click 事件中
this.btnCancel.Click += (s, e) =>
{
this.IsCancelled = true;
this.lblMessage.Text = "正在取消排版,请稍候...";
// ★★★ 强制刷新界面(非常重要!) ★★★
this.Refresh();
Application.DoEvents();
};
---------------------------------------- 复制结束 ----------------------------------------
新手常见错误:只设置了 IsCancelled = true,但没有调用 this.Refresh() 和 Application.DoEvents()。
界面看起来像"卡死",实际已经点中了,但界面没刷新。
【第 3 步:修改命令入口,重置标志】
找到你的 CommandMethod 入口函数,在最开头加上:
---------------------------------------- 复制开始 ----------------------------------------
CommandMethod("XX")
public void RunCommand()
{
// ★★★ 第一件事:重置全局取消标志 ★★★
GlobalCancel.Reset();
_cancelRequested = false;
// ... 然后才是业务代码 ...
// ... 最后调用异步函数 ...
}
---------------------------------------- 复制结束 ----------------------------------------
新手常见错误:只在异步函数开头重置,没有在命令入口重置。
如果上一次排版中途取消了,下一次运行命令时,异步函数还没执行到重置代码,
后台任务就已经被之前的取消标志"杀死"了,导致"一运行就自动取消"。
【第 4 步:修改核心异步编排函数】
找到你的 RunDirectNestingRectAsync(或类似名称),按照以下模板修改。
---------------------------------------- 复制开始 ----------------------------------------
private void RunDirectNestingRectAsync(Database db, Editor ed, List<PartData> parts, Point2d basePoint)
{
// ========================================================================
// 第一部分:初始化(重置标志 + 清空消息队列)
// ========================================================================
// 1. 重置取消标志(再次确保)
GlobalCancel.Reset();
_cancelRequested = false;
// 2. 清空旧的消息队列
while (_messageQueue.TryDequeue(out _)) { }
// 3. 创建并显示进度窗体
var progress = new ProgressForm();
progress.SetMessage("正在初始化...");
progress.Show();
Application.DoEvents();
// 4. 定义共享数据容器(线程安全需加锁)
var placed = new List<PlacementResult>();
var sheets = new List<SheetData>();
int sheetIndex = 0;
int totalPlaced = 0;
bool isFinished = false;
string errorMsg = null;
// ========================================================================
// 第二部分:启动后台计算任务(Task.Run)
// ========================================================================
var backgroundTask = Task.Run(() =>
{
try
{
var localParts = allParts;
while (localParts.Count > 0 && sheetIndex < 1000)
{
// ★★★ 每次循环开始检查取消标志 ★★★
if (GlobalCancel.IsRequested || _cancelRequested)
break;
// ... 你的业务计算代码 ...
for (int iter = 0; iter < MaxIterations; iter++)
{
// ★★★ 每次迭代前检查 ★★★
if (GlobalCancel.IsRequested || _cancelRequested)
break;
// ... 业务计算 ...
// ★★★ 更新进度(跨线程 + 异常保护) ★★★
try
{
progress.Invoke((MethodInvoker)delegate
{
progress.UpdateProgress(iter + 1, MaxIterations);
});
}
catch (ObjectDisposedException) { break; }
catch (InvalidOperationException) { break; }
}
if (GlobalCancel.IsRequested || _cancelRequested)
break;
// ... 提取结果 ...
// ★★★ 日志入队(禁止直接 ed.WriteMessage) ★★★
_messageQueue.Enqueue($"\n母材 #{sheetIndex + 1} 放置 {placedCount} 个");
sheetIndex++;
}
// ★★★ 无论正常结束还是取消,都要标记完成 ★★★
isFinished = true;
}
catch (Exception ex)
{
errorMsg = ex.Message;
isFinished = true; // ★★★ 异常也标记完成 ★★★
}
});
// ========================================================================
// 第三部分:启动 UI 轮询定时器(200ms)
// ========================================================================
_pollTimer = new System.Windows.Forms.Timer();
_pollTimer.Interval = 200;
_pollTimer.Tick += (s, e) =>
{
// 1. 输出消息队列中的日志
while (_messageQueue.TryDequeue(out string msg))
ed.WriteMessage(msg);
// ================================================================
// ★★★ 取消逻辑:检测到用户点击了取消按钮 ★★★
// ================================================================
if (progress.IsCancelled)
{
// a) 通知后台线程停止
GlobalCancel.IsRequested = true;
_cancelRequested = true;
// b) 立即输出取消信息
ed.WriteMessage($"\n✅ 排版已取消,共使用 {sheetIndex} 块母材,已放置 {totalPlaced} 个零件。");
// c) ★★★ 立即关闭进度窗体(不等待后台任务) ★★★
progress.Close();
// d) 停止定时器
_pollTimer.Stop();
_pollTimer = null;
return;
}
// 检查后台任务是否完成
if (backgroundTask.IsCompleted || backgroundTask.IsFaulted)
{
_pollTimer.Stop();
if (errorMsg != null)
ed.WriteMessage($"\n错误: {errorMsg}");
else if (isFinished && !progress.IsCancelled)
{
ed.WriteMessage($"\n排版结束,共使用 {sheetIndex} 块母材,成功放置 {totalPlaced} 个零件。");
// 绘图(加锁保护)
List<SheetData> sheetCopy;
List<PlacementResult> placedCopy;
lock (sheets) { sheetCopy = new List<SheetData>(sheets); }
lock (placed) { placedCopy = new List<PlacementResult>(placed); }
DrawResult(db, ed, placedCopy, sheetCopy);
}
progress.Close();
_pollTimer = null;
}
};
_pollTimer.Start();
}
---------------------------------------- 复制结束 ----------------------------------------
新手常见错误 1:在取消分支里没有立即关闭窗体,而是 return 等待后台任务。
必须:检测到取消 → 立即 Close()。
新手常见错误 2:在后台任务中直接写 ed.WriteMessage。
必须:用 _messageQueue.Enqueue() 入队。
【第 5 步:在深层业务逻辑中添加取消检查】
在 Background.placeParts、NestingContext.NestIterate、SvgNest.launchWorkers 中,
所有循环入口添加检查:
---------------------------------------- 复制开始 ----------------------------------------
// Background.placeParts
public static SheetPlacement placeParts(...)
{
if (GlobalCancel.IsRequested) return null;
while (parts.Length > 0)
{
if (GlobalCancel.IsRequested) return null;
for (int i = 0; i < parts.Length; i++)
{
if (GlobalCancel.IsRequested) break;
// ... 业务代码 ...
}
}
return result;
}
// NestingContext.NestIterate
public void NestIterate()
{
if (GlobalCancel.IsRequested) return;
// ... 拷贝数据 ...
if (GlobalCancel.IsRequested) return;
Nest.launchWorkers(partsLocal.ToArray());
}
---------------------------------------- 复制结束 -----------------------------------------
═══════════════════════════════════════════════════════════════════════════════
第四部分:完整代码骨架(5 个关键文件)
═══════════════════════════════════════════════════════════════════════════════
【文件 1:GlobalCancel.cs】
─────────────────────────────────────────────────────────────────────────────
using System;
namespace DeepNestLib
{
public static class GlobalCancel
{
public static volatile bool IsRequested = false;
public static void Reset() => IsRequested = false;
}
}
【文件 2:ProgressForm.cs(节选)】
─────────────────────────────────────────────────────────────────────────────
using System;
using System.Windows.Forms;
public class ProgressForm : Form
{
private Button btnCancel;
private Label lblMessage;
private ProgressBar progressBar;
public bool IsCancelled { get; private set; } = false;
private void InitializeComponent()
{
this.btnCancel = new Button();
this.lblMessage = new Label();
// ... 其他控件初始化 ...
this.btnCancel.Text = "取消排版";
this.btnCancel.Click += (s, e) =>
{
this.IsCancelled = true;
this.lblMessage.Text = "正在取消排版,请稍候...";
this.Refresh();
Application.DoEvents();
};
}
public void SetMessage(string msg)
{
this.lblMessage.Text = msg;
Application.DoEvents();
}
public void UpdateProgress(int current, int total)
{
if (total == 0) return;
int percent = (int)((double)current / total * 100);
this.progressBar.Value = percent;
this.progressBar.Refresh();
Application.DoEvents();
}
}
【文件 3:Background.cs(节选)】
─────────────────────────────────────────────────────────────────────────────
namespace DeepNestLib
{
public class Background
{
public static SheetPlacement placeParts(NFP\[\] sheets, NFP\[\] parts,
SvgNestConfig config, int nestindex)
{
if (GlobalCancel.IsRequested) return null;
while (parts.Length > 0)
{
if (GlobalCancel.IsRequested) return null;
for (int i = 0; i < parts.Length; i++)
{
if (GlobalCancel.IsRequested) break;
// ... 业务逻辑 ...
}
}
return result;
}
public void BackgroundStart(DataInfo data)
{
if (GlobalCancel.IsRequested) return;
// ... 启动计算 ...
}
public void sync()
{
if (GlobalCancel.IsRequested) return;
var placement = placeParts(...);
if (placement == null || GlobalCancel.IsRequested) return;
ResponseAction(placement);
}
}
}
【文件 4:NestingContext.cs(节选)】
─────────────────────────────────────────────────────────────────────────────
public class NestingContext
{
public void NestIterate()
{
if (GlobalCancel.IsRequested) return;
// ... 拷贝数据 ...
if (GlobalCancel.IsRequested) return;
Nest.launchWorkers(partsLocal.ToArray());
}
}
【文件 5:SvgNest.cs(节选)】
─────────────────────────────────────────────────────────────────────────────
public class SvgNest
{
public void launchWorkers(NestItem\[\] parts)
{
if (GlobalCancel.IsRequested) return;
// ... 准备数据 ...
if (GlobalCancel.IsRequested) return;
background.BackgroundStart(data);
}
}
═══════════════════════════════════════════════════════════════════════════════
第五部分:常见错误与排查方法
═══════════════════════════════════════════════════════════════════════════════
错误 1:点击取消后窗口没关闭,但命令行有取消信息
原因:定时器收尾分支中,progress.Close() 被异常跳过。
排查:检查 progress.Close() 之前的代码是否抛出了异常(如 DrawResult 中绘图失败)。
解决:把 progress.Close() 放在 try-finally 中,或放在取消分支中先执行关闭再绘图。
错误 2:点击取消后窗口关闭了,但后台任务还在疯狂跑 CPU
原因:后台任务的循环中没有正确检查取消标志,或者检查点离得太远。
排查:在循环入口添加日志,确认是否进入了循环。
解决:在所有 while、for、foreach 入口添加检查。
错误 3:第二次运行排版时,还没点击取消就自动取消了
原因:全局取消标志没有重置,保留着上一次的 true 值。
排查:在命令入口和异步函数开头添加日志,输出 GlobalCancel.IsRequested 的值。
解决:在命令入口处强制重置为 false。
错误 4:跨线程更新进度条时程序崩溃
原因:进度窗体已被关闭,但后台线程仍在调用 progress.Invoke。
排查:查看异常类型(通常是 ObjectDisposedException)。
解决:用 try-catch 包裹 Invoke,捕获这两种异常并 break。
错误 5:定时器 Tick 中访问 placed 或 sheets 时出现异常
原因:后台线程正在修改这些集合,同时定时器在读取。
排查:检查是否在读取时加了 lock。
解决:在读取和写入时都使用 lock 保护。
错误 6:取消后命令行没有输出"排版已取消"
原因:定时器检测到 progress.IsCancelled 为 false。
排查:在定时器 Tick 开头添加日志,输出 progress.IsCancelled 的值。
解决:检查取消按钮的 Click 事件是否正确设置了 IsCancelled = true。
错误 7:AutoCAD 报"eLockViolation"错误
原因:在操作数据库对象时没有锁定文档,或事务嵌套错误。
排查:检查所有数据库操作是否都在 using (DocumentLock) 和 using (Transaction) 中。
解决:确保 DrawResult 等函数中用 using (DocumentLock docLock = doc.LockDocument())。
错误 8:原生 DLL 无法中断(minkowski.dll 等)
原因:C# 代码无法中断正在执行的 C++ 原生代码。
解决:这是无法绕过的限制,只能等待当前调用返回。可在文档中说明。
═══════════════════════════════════════════════════════════════════════════════
第六部分:新手最容易忽略的 3 个致命细节(一定要看)
═══════════════════════════════════════════════════════════════════════════════
致命细节 1:静态变量未重置
现象:第二次运行命令,还没点取消就自动停止。
原因:上一次取消后 GlobalCancel.IsRequested 一直为 true。
解决:在 CommandMethod 入口函数的第一行调用 GlobalCancel.Reset()。
致命细节 2:取消时没有立即关闭窗体
现象:点了取消,命令行输出了取消信息,但进度窗体一直不关。
原因:在定时器的取消分支中,只设置了标志,没有调用 progress.Close()。
解决:在取消分支中,设置标志后立即调用 progress.Close()。
致命细节 3:后台任务中直接操作 UI 或 AutoCAD Editor
现象:运行时报"调用线程无法访问此对象"或"eLockViolation"。
原因:后台线程不能直接操作 UI 控件或 ed.WriteMessage。
解决:UI 用 Invoke,日志用消息队列,数据库操作用 DocumentLock。
═══════════════════════════════════════════════════════════════════════════════
第七部分:快速参考卡片(打印贴在工位)
═══════════════════════════════════════════════════════════════════════════════
【取消标志定义】
public static volatile bool IsRequested = false;
【取消标志重置】
GlobalCancel.IsRequested = false;
【取消标志检查】
if (GlobalCancel.IsRequested) break;
【取消标志设置】
GlobalCancel.IsRequested = true;
【消息队列定义】
private static ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
【消息入队(后台线程)】
_messageQueue.Enqueue("消息");
【消息出队(UI 线程)】
while (_messageQueue.TryDequeue(out string msg)) { ed.WriteMessage(msg); }
【跨线程更新 UI】
try { progress.Invoke((MethodInvoker)delegate { progress.UpdateProgress(...); }); }
catch (ObjectDisposedException) { break; }
catch (InvalidOperationException) { break; }
【定时器创建与启动】
_pollTimer = new System.Windows.Forms.Timer();
_pollTimer.Interval = 200;
_pollTimer.Tick += (s, e) => { ... };
_pollTimer.Start();
【定时器停止与释放】
_pollTimer.Stop();
_pollTimer = null;
【取消立即响应(定时器 Tick 中)】
if (progress.IsCancelled)
{
GlobalCancel.IsRequested = true;
ed.WriteMessage("✅ 已取消");
progress.Close();
_pollTimer.Stop();
_pollTimer = null;
return;
}
═══════════════════════════════════════════════════════════════════════════════
第八部分:如何用测试代码验证(不用跑完整排样)
═══════════════════════════════════════════════════════════════════════════════
如果你不想跑复杂的排样,只想验证"取消框架"是否搭建成功,用 Thread.Sleep 模拟耗时任务:
---------------------------------------- 复制开始 ----------------------------------------
// 在后台任务中替换为以下测试代码
for (int i = 0; i < 100; i++)
{
if (GlobalCancel.IsRequested) break;
Thread.Sleep(100); // 模拟耗时 100ms
progress.Invoke((MethodInvoker)delegate
{
progress.UpdateProgress(i + 1, 100);
});
}
---------------------------------------- 复制结束 ----------------------------------------
验证标准:
点击取消后,进度条能在 1 秒内停止并关闭窗体 → 取消框架搭建成功。
═══════════════════════════════════════════════════════════════════════════════
第九部分:最终检查清单(上线前验证)
═══════════════════════════════════════════════════════════════════════════════
□ 1. 所有耗时循环都有取消检查点(while、for、foreach)
□ 2. 取消时窗体能在 1 秒内关闭
□ 3. 取消后命令行有明确的"已取消"提示
□ 4. 取消后后台任务能在下一次检查点退出(观察 CPU 占用下降)
□ 5. 取消后再次运行排版,不会自动取消(标志已重置)
□ 6. 连续多次取消再运行,程序稳定不崩溃
□ 7. 正常完成排版,不会输出"已取消"的误报
□ 8. 绘图结果正确,没有因为取消而残留错误图形
□ 9. 所有数据库事务正确提交或回滚,没有锁残留
□ 10. 进度条数字正确,不会因为取消而显示异常值
═══════════════════════════════════════════════════════════════════════════════
第十部分:文件修改优先级表
═══════════════════════════════════════════════════════════════════════════════
优先级 1(必须改):
Background.cs - 添加取消标志和循环检查
NestingContext.cs - 迭代入口添加快速失败
SvgNest.cs - Worker 启动前检查取消标志
优先级 2(核心改):
AAAdn排版net48支持凸度.cs - 异步编排 + 定时器 + 消息队列
优先级 3(界面改):
NestConfigDialog.cs - 进度窗体取消按钮
优先级 4(新建):
GlobalCancel.cs - 全局取消标志(如果没有的话)
═══════════════════════════════════════════════════════════════════════════════
第十一部分:总结
═══════════════════════════════════════════════════════════════════════════════
通过 协作式取消 + 后台线程 + UI 定时器 + 消息队列 的组合模式,
可以在任何大型 .NET 项目中实现"点击取消立即响应"的用户体验。
核心只有 4 个步骤:
-
定义全局取消标志。
-
在耗时循环中主动检查。
-
使用后台线程 + 定时器分离 UI 和计算。
-
取消时立即关闭 UI,不等待后台。
记住一句话:"让计算主动让路,而不是让用户等待。"
这套方案已在 AutoCAD 排样插件中成功实施,有效提升了用户体验,
且代码可维护性良好。对于包含第三方原生库的场景,可进一步考虑
进程隔离方案,但多数情况下上述模式已足够。
================================================================================
文档结束
================================================================================