Skip to content
C#
异步编程

异步编程

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.TimerApplication.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 是安全的。``