# 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 /// /// 在专用 STA 后台线程中异步运行 VisionPro ToolBlock。 /// 避免 COM 封送到 UI 线程导致界面假死。 /// public static Task RunToolBlockAsync(CogToolBlock toolBlock) { var tcs = new TaskCompletionSource(); 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 阻塞。 --- ## 7. 详细修改计划 ### 7.1 实施概览 ```mermaid gantt title 修改实施甘特图 dateFormat YYYY-MM-DD axisFormat %m/%d section Phase 1 - 基础设施 Step1-创建RunToolBlockAsync工具方法 :a1, 2026-07-12, 1d section Phase 2 - P0 核心修复 Step2-修改RemoteCommandService.cs(6处) :a2, after a1, 2d Step3-编译验证+本地冒烟测试 :a3, after a2, 1d section Phase 3 - P1 并发优化 Step4-修改Management.cs ToolBlock并发执行 :a4, after a3, 1d section Phase 4 - P2 VPP加载 Step5-修改ProductService.cs异步加载 :a5, after a4, 1d section Phase 5 - P3 Countdown Step6-修改Management.cs countdown异步等待 :a6, after a5, 1d section Phase 6 - 集成测试 Step7-完整流程回归测试 :a7, after a6, 2d ``` ### 7.2 前置条件 - [ ] 确认开发环境可正常编译当前代码 - [ ] 准备至少一台可连接的真实相机和 PLC 用于集成测试 - [ ] 准备 2~3 个包含有效 `.vpp` ToolBlock 的测试产品 - [ ] 确保 VisionPro 运行时许可在测试机上可用 - [ ] 建议在版本控制中创建分支 `fix/toolblock-ui-blocking` 进行修改 ### 7.3 分步实施详情 --- #### Step 1:创建 `RunToolBlockAsync` 工具方法 **目标**:提供一个可复用的 STA 线程异步执行 `ToolBlock.Run()` 的基础设施。 **新增文件**:`TeamAAS-VM/Helpers/VisionProHelper.cs`(推荐路径) **代码清单**: ```csharp // TeamAAS-VM/Helpers/VisionProHelper.cs using Cognex.VisionPro.ToolBlock; using System; using System.Threading; using System.Threading.Tasks; namespace TeamAAS_VP.Helpers { /// /// VisionPro 工具辅助类。 /// 提供在独立 STA 线程中异步执行 CogToolBlock 的能力, /// 避免 COM 封送到主 UI 线程导致界面假死。 /// public static class VisionProHelper { /// /// 在专用 STA 后台线程中异步运行 ToolBlock。 /// 每次调用创建一个新的 STA 线程,ToolBlock 执行完毕后线程自动退出。 /// /// 要执行的 CogToolBlock 实例 /// 异步任务,完成时表示 ToolBlock 执行完毕 public static Task RunToolBlockAsync(CogToolBlock toolBlock) { if (toolBlock == null) throw new ArgumentNullException(nameof(toolBlock)); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { try { toolBlock.Run(); tcs.SetResult(true); } catch (Exception ex) { tcs.SetException(ex); } }) { IsBackground = true, Name = $"VP-Run-{Guid.NewGuid():N}" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); return tcs.Task; } /// /// 在专用 STA 后台线程中异步加载 VisionPro 序列化对象(.vpp 文件等)。 /// /// 反序列化的目标类型 /// .vpp 文件完整路径 /// 包含反序列化对象的异步任务 public static Task LoadVisionProObjectAsync(string filePath) where T : class { if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { try { var obj = CogSerializer.LoadObjectFromFile(filePath) as T; tcs.SetResult(obj); } catch (Exception ex) { tcs.SetException(ex); } }) { IsBackground = true, Name = $"VP-Load-{Guid.NewGuid():N}" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); return tcs.Task; } /// /// 在专用 STA 后台线程中异步保存 VisionPro 对象到文件。 /// /// 要保存的对象 /// 目标文件路径 /// 异步任务 public static Task SaveVisionProObjectAsync(object obj, string filePath) { if (obj == null) throw new ArgumentNullException(nameof(obj)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { try { CogSerializer.SaveObjectToFile(obj, filePath); tcs.SetResult(true); } catch (Exception ex) { tcs.SetException(ex); } }) { IsBackground = true, Name = $"VP-Save-{Guid.NewGuid():N}" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); return tcs.Task; } } } ``` **验证**:编译通过即可,该文件无外部依赖(仅依赖 VisionPro SDK)。 --- #### Step 2(P0):修改 `RemoteCommandService.cs` — 所有 `ToolBlock.Run()` 替换为异步 **目标**:将所有 6 处同步 `ToolBlock.Run()` 替换为 `await VisionProHelper.RunToolBlockAsync()`。 **涉及文件**:`TeamAAS-VM/Services/RemoteCommandService.cs` **修改点清单**: | 序号 | 行号 | 所在方法 | 修改内容 | |------|------|----------|----------| | 2.1 | 1526 | `ExecutePhoto(procedure, out outputCollection)` | 方法签名改为 `async Task`,`ToolBlock.Run()` → `await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock)` | | 2.2 | 1690 | `ExecutePhoto(procedure, InputTerminal, out outputCollection)` | 同 2.1 | | 2.3 | 1856 | `ExecutePhoto2(procedure, InputTerminal, out outputCollection)` | 同 2.1 | | 2.4 | 2120 | `ExecutePhoto(procedure, imges, InputTerminal, ..., is3dmodel, ...)` | AI 推理路径内的 `ToolBlock.Run()` 改为 `await ...` | | 2.5 | 2206 | `ExecutePhoto(procedure, imges, InputTerminal, ..., is3dmodel, ...)` | 非 AI 路径内的 `ToolBlock.Run()` 改为 `await ...` | | 2.6 | 2985 | `ExecutePhotoEx(...)` | 方法签名改为 `async Task`,`ToolBlock.Run()` → `await ...` | **详细修改:修改点 2.5(最关键的生产路径)**: > 这是 `PhotosUse == false` 模式下生产主流程使用的路径,影响最大。 *修改前(约第 2198~2260 行区域)*: ```csharp else { // 3. 运行视觉工具 LogHelper.WriteLogInfo("开始运行视觉工具"); procedure.ToolBlock.Run(); //运行ToolBlock if (procedure.ToolBlock.RunStatus.Result == CogToolResultConstants.Accept) { // ... 结果处理 ... } } ``` *修改后*: ```csharp else { // 3. 运行视觉工具(在独立 STA 线程中异步执行,避免阻塞 UI 线程) LogHelper.WriteLogInfo("开始运行视觉工具"); await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock); if (procedure.ToolBlock.RunStatus.Result == CogToolResultConstants.Accept) { // ... 结果处理(保持不变) ... } } ``` **级联影响**:`ExecutePhoto` 系列方法返回类型从 `bool` 变为 `Task`,其调用方需要做以下适配: | 调用方 | 文件 | 行号 | 适配方式 | |--------|------|------|----------| | `ExecutePhotoGetSinglePoint1` | `RemoteCommandService.cs` | 2781 | 加 `await` | | `ExecutePhotoGetSinglePoint2` | `RemoteCommandService.cs` | 2866 | 加 `await`(`ExecutePhoto2`) | | `ExecutePhotoGetSinglePoint` | `RemoteCommandService.cs` | 3037 | 加 `await` | | 第二处 `ExecutePhotoGetSinglePoint1` | `RemoteCommandService.cs` | 3126 | 加 `await` | | `ExecutePhotoGetSinglePointWithImage` | `RemoteCommandService.cs` | 3217 | 加 `await`(`ExecutePhotoEx`) | 这些调用方全部已经是 `async Task` 方法,只需在调用处加 `await` 即可,改动量很小。 --- #### Step 3(P0 后续):编译验证 + 本地冒烟测试 **编译验证**: ```powershell dotnet build TeamAAS-VM.sln ``` **冒烟测试检查点**: 1. [ ] 手动加载一个产品,观察 UI 是否响应正常 2. [ ] 手动点击"单次执行"按钮运行一个 ToolBlock,确认 UI 不卡顿 3. [ ] 模拟触发一个 AOI 点位(`cameraIndex=1`),检查: - 图像是否正常采集 - ToolBlock 是否正常执行 - UI 在整个过程中是否保持响应 - PLC 信号(`Out_FixedCameraPickStatus`)是否正确写入 4. [ ] 检查日志中 STA 线程命名(`VP-Run-xxx`)是否正常出现 --- #### Step 4(P1):修改 `Management.cs` — 多 ToolBlock 真正并发执行 **涉及文件**:`TeamAAS-VM/Core/Management.cs` **修改位置**:约第 2610~2635 行,图像批处理完成后的 `foreach` 循环 *修改前*: ```csharp _ = Task.Run(async () => { var imageBatchWatch = Stopwatch.StartNew(); var ImageTaskResults = await Task.WhenAll(_ImageTaskList); imageBatchWatch.Stop(); // ... foreach (var item in ImageTaskResults) // ← 串行 { if (item.IsSucceed) { bool Is3d = Is3D(item.procedureId); if (targetPoint.AiVision == 1) { var targetPoint2 = currentProduct.AoiPoints?.FirstOrDefault(p => p.Number == cameraIndex); await ExecuteProcedureAsync(targetPoint.AiVision, targetPoint2.CameraProcedureId2, item.Images, Is3d, currentProduct); } await ExecuteProcedureAsync(item.cameraIndex, item.procedureId, item.Images, Is3d, currentProduct); } // ... } }); ``` *修改后*: ```csharp _ = Task.Run(async () => { var imageBatchWatch = Stopwatch.StartNew(); var ImageTaskResults = await Task.WhenAll(_ImageTaskList); imageBatchWatch.Stop(); // ... // 并发执行所有 ToolBlock(每个 ToolBlock 在独立 STA 线程中运行) var procedureTasks = new List(); foreach (var item in ImageTaskResults) { if (item.IsSucceed) { bool Is3d = Is3D(item.procedureId); if (targetPoint.AiVision == 1) { var targetPoint2 = currentProduct.AoiPoints?.FirstOrDefault(p => p.Number == cameraIndex); procedureTasks.Add(ExecuteProcedureAsync(targetPoint.AiVision, targetPoint2.CameraProcedureId2, item.Images, Is3d, currentProduct)); } procedureTasks.Add(ExecuteProcedureAsync(item.cameraIndex, item.procedureId, item.Images, Is3d, currentProduct)); } else { SendTaskMessage($"图像采集失败!{item.cameraIndex}: {item.procedureId}", MessageLevel.Alarm); AllResult.Add(false); ClearCountdown(); } } // 等待所有并发 ToolBlock 执行完成 await Task.WhenAll(procedureTasks); }); ``` > ⚠️ **注意**:因为 Step 2 已经确保每个 `ToolBlock.Run()` 在独立 STA 线程中执行,且每个 AOI 点位使用不同的 `ProcedureModel`(即不同的 ToolBlock 实例),所以多线程并发执行不同 ToolBlock 是安全的。 --- #### Step 5(P2):修改 `ProductService.cs` — VPP 加载/保存异步化 **涉及文件**:`TeamAAS-VM/Services/ProductService.cs` **修改点**: | 序号 | 行号 | 方法 | 修改 | |------|------|------|------| | 5.1 | ~530 | `LoadProduct(Guid id)` | `CogSerializer.LoadObjectFromFile` → `VisionProHelper.LoadVisionProObjectAsync` | | 5.2 | ~597 | `LoadProductAsync(Guid id)` | `_vppCache.GetOrAdd` 回调中的同步加载改为使用 `VisionProHelper` | | 5.3 | ~481 | `SaveProduct(ProductModel p, string filePath)` | `CogSerializer.LoadObjectFromFile` / `CogSerializer.SaveObjectToFile` → `VisionProHelper` 异步方法 | **详细修改:5.2 示例**: *修改前(`LoadProductAsync`,约第 564~600 行)*: ```csharp prc.ToolBlock = _vppCache.GetOrAdd(prcPath, path => { return CogSerializer.LoadObjectFromFile(path) as CogToolBlock; }); ``` *修改后*: ```csharp // 异步加载 VPP,避免同步 I/O 阻塞调用线程 prc.ToolBlock = await _vppCache.GetOrAddAsync(prcPath, async path => { return await VisionProHelper.LoadVisionProObjectAsync(path); }); ``` > ⚠️ **注意**:需要使用支持异步回调的 `GetOrAddAsync` 扩展方法,或自行实现。若 `ConcurrentDictionary` 原生不支持,可改为带锁的 `Dictionary` + 异步判断: ```csharp if (!_vppCache.TryGetValue(prcPath, out var cached)) { cached = await VisionProHelper.LoadVisionProObjectAsync(prcPath); _vppCache.TryAdd(prcPath, cached); } prc.ToolBlock = cached; ``` --- #### Step 6(P3):修改 `Management.cs` — `countdown.Wait()` 改为异步 **涉及文件**:`TeamAAS-VM/Core/Management.cs` **修改位置**:约第 2650 行 *修改前*: ```csharp bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5)); ``` *修改后*: ```csharp // 异步等待 countdown,避免同步阻塞线程池线程 bool countdownCompleted; using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { try { await Task.Run(() => countdown.Wait(cts.Token), cts.Token); countdownCompleted = true; } catch (OperationCanceledException) { countdownCompleted = false; } } ``` --- #### Step 7:完整流程回归测试 **测试场景矩阵**: | 场景 | 描述 | 预期结果 | |------|------|----------| | TC-01 | 单点位单 ToolBlock 触发 | ToolBlock 正常执行,UI 不卡顿,PLC 信号正确 | | TC-02 | 单点位多 ToolBlock (`PhotosUse=false`) | 多个 ToolBlock 并发执行,总耗时约等于最慢的那个,UI 不卡顿 | | TC-03 | 多点位顺序触发(模拟 1→2→3→stop) | 每个位点 ToolBlock 按时序执行,UI 持续响应 | | TC-04 | 产品加载/切换 | 加载新产品的 VPP 不阻塞 UI,ToolBlock 就绪后可正常执行 | | TC-05 | 连续 20 次触发循环 | 无内存泄漏,无线程泄漏(STA 线程正常退出) | | TC-06 | 异常 case:VPP 文件损坏 | 异常正确捕获,`countdown.Signal()` 正常调用,不卡死 | | TC-07 | 超时 case:ToolBlock 执行超时 | `countdown` 超时机制正常,按 NG 处理 | **测试工具**: - 在 `SendTaskMessage` 日志中检查 STA 线程命名 `VP-Run-xxx` - 在任务管理器中观察线程数量变化(每次 ToolBlock 执行完毕后 STA 线程应退出) - 在 `PlcCommandTriggerAsync` 入口处已有的 `[PLC-TRACE]` 日志中对比修改前后的时序 --- ### 7.4 回滚方案 如果修改后出现不可预期的问题,可执行以下回滚步骤: 1. **Git 回滚**:`git revert` 对应的 commit,或切换到修改前的分支 2. **If 仅需禁用 STA 线程化**:在 `VisionProHelper.RunToolBlockAsync` 中添加配置开关: ```csharp public static async Task RunToolBlockAsync(CogToolBlock toolBlock) { // 紧急回滚开关:从 app.config 读取 if (ConfigurationManager.AppSettings["VisionPro:UseStaThread"] == "false") { toolBlock.Run(); // 回退到原始同步调用 return; } // ... STA 线程逻辑 ... } ``` --- ### 7.5 潜在风险与缓解措施 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|----------| | **同一 ToolBlock 实例被多线程并发调用** | 中 | 崩溃或结果错乱 | 确认每个 AOI 点位的 `ProcedureModel` 使用不同 ToolBlock 实例;若不成立,在 P1 改为按位点串行 | | **STA 线程过多导致资源耗尽** | 低 | 内存增长、调度延迟 | 当前场景最多同时 ~8 个 ToolBlock,STA 线程数可控;若未来扩展,可引入 `SemaphoreSlim` 限流 | | **`TaskCompletionSource` 未正确释放** | 低 | 内存泄漏 | STA 线程 `finally` 块中确保 `TrySetResult`/`TrySetException` | | **现有调用方依赖同步返回结果** | 低 | 编译错误 | 所有调用方已在 `async Task` 上下文中,仅需加 `await` | | **VisionPro 许可检查在 STA 线程中失败** | 低 | ToolBlock 无法执行 | 保留原始异常信息,通过 `tcs.SetException` 向上传递 | --- ### 7.6 预估工作量 | 步骤 | 内容 | 预估工时 | |------|------|----------| | Step 1 | 创建 `VisionProHelper.cs` | 0.5h | | Step 2 | 修改 `RemoteCommandService.cs`(6 处 + 5 处调用方 await) | 2h | | Step 3 | 编译验证 + 冒烟测试 | 1h | | Step 4 | 修改 `Management.cs` 并发执行 | 1h | | Step 5 | 修改 `ProductService.cs` VPP 加载 | 1.5h | | Step 6 | 修改 `Management.cs` countdown | 0.5h | | Step 7 | 完整流程回归测试 | 3h | | **合计** | | **约 9.5h(1~2 个工作日)** |