|
|
@@ -0,0 +1,255 @@
|
|
|
+# VisionPro ToolBlock 加载/运行导致 UI 阻塞排查报告
|
|
|
+
|
|
|
+> 日期:2026-07-12
|
|
|
+> 涉及文件:`TeamAAS-VM/Services/ProductService.cs`、`TeamAAS-VM/Core/Management.cs`(~1640 行)、`TeamAAS-VM/Services/RemoteCommandService.cs`
|
|
|
+> 问题:加载 VPP 和运行 ToolBlock 时 UI 假死,尤其多 ToolBlock 串行场景下更加明显
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 场景描述
|
|
|
+
|
|
|
+生产流程中触发固定相机检测时,会按以下顺序执行:
|
|
|
+
|
|
|
+1. 收到 PLC 触发信号 → 相机采集图像 → 告知 PLC 采集完成
|
|
|
+2. 图像传入 ToolBlock 1 运行视觉流程(同时继续接收下一个触发信号)
|
|
|
+3. 收到第二个触发信号 → 采集图像 → ToolBlock 2 运行
|
|
|
+4. 收到第三个触发信号 → 采集图像 → ToolBlock 3 运行
|
|
|
+5. ……以此类推
|
|
|
+
|
|
|
+在此过程中可能同时存在多个 ToolBlock 在不同阶段执行,但 UI 完全失去响应。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 根因分析
|
|
|
+
|
|
|
+### 🔴 根因 1(最核心):`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()` 均是同步阻塞调用,无一使用后台线程或异步封装。**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 🔴 根因 2:VPP 文件加载也同步阻塞
|
|
|
+
|
|
|
+**在 `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。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 🟡 根因 3:多 ToolBlock 串行执行,非真正并发
|
|
|
+
|
|
|
+**在 `Management.cs` 第 2520~2640 行区域**:
|
|
|
+
|
|
|
+**分支 A**(`sysConfig.PhotosUse == true`,即"逐点采图+逐点处理"模式,第 2547 行):
|
|
|
+```csharp
|
|
|
+foreach (var prcId in procIds)
|
|
|
+{
|
|
|
+ var tt = await GrabImageEx(...); // 采图(同步等待)
|
|
|
+ await ExecuteProcedureAsync(...); // ToolBlock.Run() → 阻塞(同步等待)
|
|
|
+ // ↑ 上一个 ToolBlock 完全结束才进入下一个循环
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**分支 B**(`PhotosUse == false`,即"全部采图后批量处理"模式,第 2577 行起):
|
|
|
+- 图像采集被并发化(`Task.WhenAll`)✅
|
|
|
+- 但 ToolBlock 执行仍然串行 ❌:
|
|
|
+```csharp
|
|
|
+foreach (var item in ImageTaskResults)
|
|
|
+{
|
|
|
+ await ExecuteProcedureAsync(...); // 一个一个串行执行!
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 🟡 根因 4:`countdown.Wait()` 同步阻塞线程池线程
|
|
|
+
|
|
|
+**`Management.cs` 第 ~2650 行**:
|
|
|
+```csharp
|
|
|
+bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
|
|
|
+```
|
|
|
+这会**同步阻塞**当前线程池线程最长 5 秒。如果此线程正在处理 PLC 回调,后续 PLC 信号处理全被延迟。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 完整调用路径图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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 线程执行,且是同步阻塞的。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 修复方案
|
|
|
+
|
|
|
+### 方案 A(P0 — 最优先):将 `ToolBlock.Run()` 放入专用 STA 后台线程
|
|
|
+
|
|
|
+VisionPro 的 COM 组件要求 STA 线程,但不应使用 UI 线程。需要创建独立的 STA 后台线程:
|
|
|
+
|
|
|
+```csharp
|
|
|
+/// <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()`:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 替换前
|
|
|
+procedure.ToolBlock.Run();
|
|
|
+
|
|
|
+// 替换后
|
|
|
+await RunToolBlockAsync(procedure.ToolBlock);
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ 注意:如果同一个 `CogToolBlock` 实例需要被多个 ToolBlock 并发使用,VisionPro 可能不支持多线程同时操作同一个 ToolBlock 实例。建议为每个并发执行创建独立的 ToolBlock 实例(clone),或者在 ToolBlock 级别做串行化保护。
|
|
|
+
|
|
|
+### 方案 B(P1):多 ToolBlock 真正并发执行
|
|
|
+
|
|
|
+在 `Management.cs` 第 2620 行附近,将串行 `foreach` 改为并发 `Task.WhenAll`:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 替换串行 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`,应该可以)。
|
|
|
+
|
|
|
+### 方案 C(P2):VPP 加载异步化
|
|
|
+
|
|
|
+在 `ProductService.cs` 的 `LoadProductAsync` 中确保反序列化在后台线程:
|
|
|
+
|
|
|
+```csharp
|
|
|
+prc.ToolBlock = await Task.Run(() =>
|
|
|
+ _vppCache.GetOrAdd(prcPath, path =>
|
|
|
+ CogSerializer.LoadObjectFromFile(path) as CogToolBlock));
|
|
|
+```
|
|
|
+
|
|
|
+同时将所有同步加载路径(`Initialize`, `SetCurrentProduct`, `LoadLastProduct`)改为异步调用。
|
|
|
+
|
|
|
+### 方案 D(P3):`countdown.Wait()` 改为异步等待
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 替换同步阻塞
|
|
|
+// 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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 优先级与影响评估
|
|
|
+
|
|
|
+| 优先级 | 修改项 | 涉及文件 | 风险 | 收益 |
|
|
|
+|--------|--------|----------|------|------|
|
|
|
+| **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 处) | 低:等价语义 | 避免线程池阻塞 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 注意事项
|
|
|
+
|
|
|
+1. **VisionPro 线程安全性**:`CogToolBlock` 实例可能不支持多线程并发操作。如果多个 ToolBlock 需要同时运行,应确保使用不同的 `CogToolBlock` 实例。建议在 `ProcedureModel` 中为每个并发执行 clone 独立的 ToolBlock。
|
|
|
+
|
|
|
+2. **STA 线程数量**:方案 A 会为每次 `ToolBlock.Run()` 创建新的 STA 线程。如果 ToolBlock 执行频繁,可考虑使用 STA 线程池复用线程,减少线程创建开销。
|
|
|
+
|
|
|
+3. **`SendTaskMessage` 已改用消息队列**:从代码来看(`Management.cs:4280`),`SendTaskMessage` 已经改为 `ConcurrentQueue` + `Dispatcher.BeginInvoke` 的异步模式,不会直接阻塞 UI。这点已正确处理。
|
|
|
+
|
|
|
+4. **`PlcCommandTrigger` 入口已使用 `Task.Run` 卸载**(`Management.cs:1530`),OPC UA 回调线程不会长时间被占。但后续的 `PlcCommandTriggerAsync` 内部仍因 `ToolBlock.Run()` 的 COM 封送导致 UI 阻塞。
|