|
|
@@ -253,3 +253,475 @@ catch (OperationCanceledException)
|
|
|
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
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// VisionPro 工具辅助类。
|
|
|
+ /// 提供在独立 STA 线程中异步执行 CogToolBlock 的能力,
|
|
|
+ /// 避免 COM 封送到主 UI 线程导致界面假死。
|
|
|
+ /// </summary>
|
|
|
+ public static class VisionProHelper
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// 在专用 STA 后台线程中异步运行 ToolBlock。
|
|
|
+ /// 每次调用创建一个新的 STA 线程,ToolBlock 执行完毕后线程自动退出。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="toolBlock">要执行的 CogToolBlock 实例</param>
|
|
|
+ /// <returns>异步任务,完成时表示 ToolBlock 执行完毕</returns>
|
|
|
+ public static Task RunToolBlockAsync(CogToolBlock toolBlock)
|
|
|
+ {
|
|
|
+ if (toolBlock == null)
|
|
|
+ throw new ArgumentNullException(nameof(toolBlock));
|
|
|
+
|
|
|
+ var tcs = new TaskCompletionSource<bool>();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 在专用 STA 后台线程中异步加载 VisionPro 序列化对象(.vpp 文件等)。
|
|
|
+ /// </summary>
|
|
|
+ /// <typeparam name="T">反序列化的目标类型</typeparam>
|
|
|
+ /// <param name="filePath">.vpp 文件完整路径</param>
|
|
|
+ /// <returns>包含反序列化对象的异步任务</returns>
|
|
|
+ public static Task<T> LoadVisionProObjectAsync<T>(string filePath) where T : class
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(filePath))
|
|
|
+ throw new ArgumentNullException(nameof(filePath));
|
|
|
+
|
|
|
+ var tcs = new TaskCompletionSource<T>();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 在专用 STA 后台线程中异步保存 VisionPro 对象到文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="obj">要保存的对象</param>
|
|
|
+ /// <param name="filePath">目标文件路径</param>
|
|
|
+ /// <returns>异步任务</returns>
|
|
|
+ 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<bool>();
|
|
|
+ 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<bool>`,`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<bool>`,`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<bool>`,其调用方需要做以下适配:
|
|
|
+
|
|
|
+| 调用方 | 文件 | 行号 | 适配方式 |
|
|
|
+|--------|------|------|----------|
|
|
|
+| `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<Task>();
|
|
|
+ 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<CogToolBlock>(path);
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ **注意**:需要使用支持异步回调的 `GetOrAddAsync` 扩展方法,或自行实现。若 `ConcurrentDictionary` 原生不支持,可改为带锁的 `Dictionary` + 异步判断:
|
|
|
+```csharp
|
|
|
+if (!_vppCache.TryGetValue(prcPath, out var cached))
|
|
|
+{
|
|
|
+ cached = await VisionProHelper.LoadVisionProObjectAsync<CogToolBlock>(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 个工作日)** |
|