日期:2026-07-12 涉及文件:
TeamAAS-VM/Services/ProductService.cs、TeamAAS-VM/Core/Management.cs(~1640 行)、TeamAAS-VM/Services/RemoteCommandService.cs问题:加载 VPP 和运行 ToolBlock 时 UI 假死,尤其多 ToolBlock 串行场景下更加明显
生产流程中触发固定相机检测时,会按以下顺序执行:
在此过程中可能同时存在多个 ToolBlock 在不同阶段执行,但 UI 完全失去响应。
ToolBlock.Run() 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件调用链(RemoteCommandService.cs + Management.cs):
PlcCommandTriggerAsync (async Task, 运行于线程池线程)
→ await ExecuteProcedureAsync(...) // Management.cs:2564
→ await _remoteCommandService.ExecutePhotoGetSinglePoint1(...) // Management.cs:3830
→ ExecutePhoto(procedure, imges, ...) // RemoteCommandService.cs:2781
→ procedure.ToolBlock.Run() // 同步阻塞!line 2206
虽然外层方法全部标记为 async Task,但 CogToolBlock.Run() 本身是一个同步方法,它会直接阻塞当前调用线程直到视觉流程执行完毕。
更致命的是,Cognex VisionPro 基于 COM 组件,CogToolBlock 内部需要 STA(单线程套间) 才能正常工作。WPF 应用的主 UI 线程是 STA 线程,而 Task.Run/async-await 在线程池(MTA)线程上执行。COM 运行时会自动将 ToolBlock.Run() 的调用封送(marshal)回主 UI 线程,这才是 UI 假死的直接原因。
结论:无论上层用了多少 async/await 或 Task.Run,ToolBlock.Run() 最终都在 UI 线程上执行,且是同步阻塞的。
ToolBlock.Run() 的所有调用点(均在 RemoteCommandService.cs)| 行号 | 方法 | 上下文 |
|---|---|---|
| 1526 | ExecutePhoto(procedure, out outputCollection) |
带实时采图 + ToolBlock 运行 |
| 1690 | ExecutePhoto(procedure, InputTerminal, out outputCollection) |
带实时采图 + 输入终端 + ToolBlock 运行 |
| 1856 | ExecutePhoto2(procedure, InputTerminal, out outputCollection) |
同上,变体 |
| 2120 | ExecutePhoto(procedure, imges, InputTerminal, ...) |
AI 推理路径 |
| 2206 | ExecutePhoto(procedure, imges, InputTerminal, ...) |
非 AI 路径,生产主流程使用 |
| 2985 | ExecutePhotoEx(procedure, InputTerminal, ...) |
带实时采图 + 输出图像 |
所有这 6 处 ToolBlock.Run() 均是同步阻塞调用,无一使用后台线程或异步封装。
在 ProductService.cs 中:
| 行号 | 方法 | 阻塞操作 |
|---|---|---|
| ~530 | LoadProduct(Guid id)(同步) |
CogSerializer.LoadObjectFromFile(path) 加载 .vpp |
| ~597 | LoadProductAsync(Guid id) |
_vppCache.GetOrAdd 回调中同步调用 CogSerializer.LoadObjectFromFile |
| ~95 | Initialize() |
循环调用 LoadProduct() 加载所有产品的 VPP |
| ~481 | SaveProduct(ProductModel p, string filePath) |
同步调用 CogSerializer.LoadObjectFromFile 加载模板 ToolBlock |
当通过 SetCurrentProduct() → LoadProduct() 加载 VPP 时,若调用方在 UI 线程上,反序列化会直接阻塞 UI。
在 Management.cs 第 2520~2640 行区域:
分支 A(sysConfig.PhotosUse == true,即"逐点采图+逐点处理"模式,第 2547 行):
foreach (var prcId in procIds)
{
var tt = await GrabImageEx(...); // 采图(同步等待)
await ExecuteProcedureAsync(...); // ToolBlock.Run() → 阻塞(同步等待)
// ↑ 上一个 ToolBlock 完全结束才进入下一个循环
}
分支 B(PhotosUse == false,即"全部采图后批量处理"模式,第 2577 行起):
Task.WhenAll)✅但 ToolBlock 执行仍然串行 ❌:
foreach (var item in ImageTaskResults)
{
await ExecuteProcedureAsync(...); // 一个一个串行执行!
}
countdown.Wait() 同步阻塞线程池线程Management.cs 第 ~2650 行:
bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
这会同步阻塞当前线程池线程最长 5 秒。如果此线程正在处理 PLC 回调,后续 PLC 信号处理全被延迟。
sequenceDiagram
participant PLC
participant OPC_UA_Callback
participant PlcCommandTriggerAsync
participant RemoteCommandService
participant UI_Thread as UI 线程 (STA)
participant CogToolBlock
PLC->>OPC_UA_Callback: 触发信号 (In_FixedCameracheckExe)
OPC_UA_Callback->>PlcCommandTriggerAsync: Task.Run → 线程池线程
PlcCommandTriggerAsync->>RemoteCommandService: ExecutePhotoGetSinglePoint1()
RemoteCommandService->>RemoteCommandService: ExecutePhoto()
Note over RemoteCommandService: 设置 InputImage 等终端
RemoteCommandService->>CogToolBlock: procedure.ToolBlock.Run()
Note over CogToolBlock: COM 组件需要 STA
CogToolBlock-->>UI_Thread: COM Marshal → 封送到 UI 线程执行
Note over UI_Thread: 🔴 UI 线程被同步阻塞
UI_Thread-->>CogToolBlock: 视觉流程执行完毕
CogToolBlock-->>RemoteCommandService: 返回结果
RemoteCommandService-->>PlcCommandTriggerAsync: 结果
关键点:即使调用从线程池发起,ToolBlock.Run() 通过 COM 封送回到了 UI 线程执行,且是同步阻塞的。
ToolBlock.Run() 放入专用 STA 后台线程VisionPro 的 COM 组件要求 STA 线程,但不应使用 UI 线程。需要创建独立的 STA 后台线程:
/// <summary>
/// 在专用 STA 后台线程中异步运行 VisionPro ToolBlock。
/// 避免 COM 封送到 UI 线程导致界面假死。
/// </summary>
public static Task RunToolBlockAsync(CogToolBlock toolBlock)
{
var tcs = new TaskCompletionSource<bool>();
var thread = new Thread(() =>
{
try
{
toolBlock.Run();
tcs.SetResult(true);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
thread.SetApartmentState(ApartmentState.STA); // 关键:STA 套间
thread.IsBackground = true;
thread.Start();
return tcs.Task;
}
然后在 RemoteCommandService.cs 中替换所有 6 处 procedure.ToolBlock.Run():
// 替换前
procedure.ToolBlock.Run();
// 替换后
await RunToolBlockAsync(procedure.ToolBlock);
⚠️ 注意:如果同一个
CogToolBlock实例需要被多个 ToolBlock 并发使用,VisionPro 可能不支持多线程同时操作同一个 ToolBlock 实例。建议为每个并发执行创建独立的 ToolBlock 实例(clone),或者在 ToolBlock 级别做串行化保护。
在 Management.cs 第 2620 行附近,将串行 foreach 改为并发 Task.WhenAll:
// 替换串行 foreach
var procedureTasks = ImageTaskResults
.Where(item => item.IsSucceed)
.Select(async item =>
{
bool Is3d = Is3D(item.procedureId);
await ExecuteProcedureAsync(item.cameraIndex, item.procedureId,
item.Images, Is3d, currentProduct);
});
await Task.WhenAll(procedureTasks);
⚠️ 注意:需要确保
AllResult和lockResultsAll等共享集合的线程安全性(当前已使用ConcurrentBag,应该可以)。
在 ProductService.cs 的 LoadProductAsync 中确保反序列化在后台线程:
prc.ToolBlock = await Task.Run(() =>
_vppCache.GetOrAdd(prcPath, path =>
CogSerializer.LoadObjectFromFile(path) as CogToolBlock));
同时将所有同步加载路径(Initialize, SetCurrentProduct, LoadLastProduct)改为异步调用。
countdown.Wait() 改为异步等待// 替换同步阻塞
// bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
// 改为异步等待
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
bool countdownCompleted;
try
{
await Task.Run(() => countdown.Wait(cts.Token), cts.Token);
countdownCompleted = true;
}
catch (OperationCanceledException)
{
countdownCompleted = false;
}
| 优先级 | 修改项 | 涉及文件 | 风险 | 收益 |
|---|---|---|---|---|
| P0 | ToolBlock.Run() → STA 线程异步化 |
RemoteCommandService.cs(6 处) |
中:需验证 ToolBlock 实例线程安全性 | 直接解决 UI 假死 |
| P1 | 多 ToolBlock 并发执行 | Management.cs(1 处) |
低:共享集合已线程安全 | 减少总流程耗时 |
| P2 | VPP 加载异步化 | ProductService.cs(4 处) |
低:纯 I/O 操作 | 产品切换时 UI 不卡 |
| P3 | countdown.Wait → await |
Management.cs(1 处) |
低:等价语义 | 避免线程池阻塞 |
VisionPro 线程安全性:CogToolBlock 实例可能不支持多线程并发操作。如果多个 ToolBlock 需要同时运行,应确保使用不同的 CogToolBlock 实例。建议在 ProcedureModel 中为每个并发执行 clone 独立的 ToolBlock。
STA 线程数量:方案 A 会为每次 ToolBlock.Run() 创建新的 STA 线程。如果 ToolBlock 执行频繁,可考虑使用 STA 线程池复用线程,减少线程创建开销。
SendTaskMessage 已改用消息队列:从代码来看(Management.cs:4280),SendTaskMessage 已经改为 ConcurrentQueue + Dispatcher.BeginInvoke 的异步模式,不会直接阻塞 UI。这点已正确处理。
PlcCommandTrigger 入口已使用 Task.Run 卸载(Management.cs:1530),OPC UA 回调线程不会长时间被占。但后续的 PlcCommandTriggerAsync 内部仍因 ToolBlock.Run() 的 COM 封送导致 UI 阻塞。