异步编程
AutoCAD 和 Revit 的 API 基本上是同步的(单线程 COM 模型),但插件中涉及文件 IO、网络请求、长时间计算时,异步编程可以防止 UI 假死。
核心概念
同步 vs 异步
// 同步:线程阻塞,UI 假死
byte[] ReadFileSync(string path)
{
return File.ReadAllBytes(path); // 阻塞当前线程
}
// 异步:线程不阻塞,UI 保持响应
async Task<byte[]> ReadFileAsync(string path)
{
return await File.ReadAllBytesAsync(path); // 不阻塞
}三个核心类型
| 类型 | 用途 | 示例 |
|---|---|---|
Task |
无返回值的异步操作 | Task.Delay(1000) |
Task<T> |
返回 T 的异步操作 | HttpClient.GetStringAsync() |
ValueTask<T> |
可能同步完成的高性能异步 | 减少堆分配 |
async / await 基本用法
// 异步方法标记为 async,返回 Task 或 Task<T>
public async Task<string> DownloadContentAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url); // 等待异步操作完成
return content; // 自动包装为 Task<string>
}
// 调用
public async Task ProcessAsync()
{
string html = await DownloadContentAsync("https://example.com");
Console.WriteLine(html.Length);
}🔬 底层原理:
await后,编译器将方法剩余部分生成为一个状态机(continuation)。当 await 的操作完成,状态机继续执行——可能在同一线程也可能在不同线程,取决于SynchronizationContext。
Task 的创建与组合
// Task.Run —— 将 CPU 密集型工作放到线程池
Task<int> task = Task.Run(() =>
{
int sum = 0;
for (int i = 0; i < 1000000; i++)
sum += i;
return sum;
});
int result = await task;
// Task.WhenAll —— 并行执行多个任务
var tasks = urls.Select(url => client.GetStringAsync(url));
string[] results = await Task.WhenAll(tasks);
// Task.WhenAny —— 任一完成即返回
Task<string> first = await Task.WhenAny(tasks);
Console.WriteLine($"最先返回:{await first}");
// Task.Delay —— 非阻塞等待
await Task.Delay(TimeSpan.FromSeconds(3));异常处理
// try-catch 与 await 正常配合
try
{
string data = await client.GetStringAsync("bad-url");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"请求失败:{ex.Message}");
}
// Task.WhenAll 的异常:抛出 AggregateException
try
{
await Task.WhenAll(tasks);
}
catch (Exception) // 实际捕获 AggregateException
{
// 默认解包第一个异常
}
// 逐个检查所有异常
Task allTasks = Task.WhenAll(tasks);
try { await allTasks; }
catch
{
foreach (var ex in allTasks.Exception!.InnerExceptions)
Console.WriteLine(ex.Message);
}🚨 永远不要使用
async void(除非是事件处理器)。async void方法抛出的异常无法被调用者捕获——会直接崩溃到 AppDomain 级别。
// ❌ 错误
public async void ExecuteAsync() { await DoWork(); }
// ✅ 正确
public async Task ExecuteAsync() { await DoWork(); }CancellationToken
public async Task ProcessWithCancelAsync(CancellationToken ct)
{
for (int i = 0; i < 100; i++)
{
ct.ThrowIfCancellationRequested(); // 检查并抛异常
await Task.Delay(100, ct); // 传递 token 给底层操作
Console.WriteLine($"进度:{i}%");
}
}
// 使用
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); // 5 秒后自动取消
try
{
await ProcessWithCancelAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已取消");
}AutoCAD/Revit 上下文中的异步
核心约束
🚨 AutoCAD 和 Revit 的绝大部分 API 只能在主线程调用。API 调用本身是同步的。多线程并发操作图形数据库是不被允许的。
// ❌ 错误:在子线程操作 AutoCAD 对象
await Task.Run(() =>
{
Line line = new Line(point1, point2); // 崩溃或行为未定义
doc.Add(line);
});
// ✅ 正确:异步仅在 IO 或纯计算阶段使用
public async Task ProcessDocumentAsync(Document doc)
{
// 主线程获取需要的数据
var lineIds = doc.Database.GetAllLineIds();
// 子线程做纯计算
var report = await Task.Run(() => GenerateReport(lineIds));
// 回到主线程写结果
doc.Editor.WriteMessage(report);
}IExternalEventHandler —— Revit 的异步模式
Revit 提供了 IExternalEventHandler 接口作为异步操作的桥梁:
// Revit 推荐的外部事件模式
public class MyEventHandler : IExternalEventHandler
{
public void Execute(UIApplication app)
{
// 这里在 Revit API 上下文中执行,安全调用 API
Document doc = app.ActiveUIDocument.Document;
// ... API 操作 ...
}
public string GetName() => "MyEventHandler";
}
// 创建并触发
var handler = new MyEventHandler();
var externalEvent = ExternalEvent.Create(handler);
externalEvent.Raise(); // 异步触发,Revit 在空闲时执行💡 AutoCAD 没有等价的官方异步机制。在 AutoCAD 中,长任务应使用
System.Timers.Timer或Application.Idle事件来分片执行。
ConfigureAwait(false) 与同步上下文
// 库代码中推荐使用 ConfigureAwait(false)
await client.GetStringAsync(url).ConfigureAwait(false);
// 含义:不强制回到原始 SynchronizationContext
// 在 UI 应用中,原始 Context 是 UI 线程
// ConfigureAwait(false) 允许 continuation 在线程池执行🚨 在 AutoCAD/Revit 插件中,如果你使用 ConfigureAwait(false),后续代码会在线程池执行,此时无法调用任何 AutoCAD/Revit API。要么不使用 ConfigureAwait(false),要么在 API 调用前手动切回主线程。
实际模式:进度报告
public async Task<int> HeavyCalculationAsync(IProgress<int>? progress = null)
{
int total = 100;
for (int i = 0; i < total; i++)
{
await Task.Delay(50); // 模拟工作
progress?.Report((i + 1) * 100 / total); // 报告进度
}
return total;
}
// 使用
var progress = new Progress<int>(pct =>
Console.WriteLine($"完成:{pct}%"));
await HeavyCalculationAsync(progress);Progress<T> 自动将回调 marshalling 到创建它的 SynchronizationContext(通常是 UI 线程),因此在回调中操作 AutoCAD/Revit API 是安全的。``