VisionPro-ToolBlock-UI阻塞排查报告.md 27 KB

VisionPro ToolBlock 加载/运行导致 UI 阻塞排查报告

日期:2026-07-12 涉及文件:TeamAAS-VM/Services/ProductService.csTeamAAS-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/awaitTask.RunToolBlock.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 行区域

分支 AsysConfig.PhotosUse == true,即"逐点采图+逐点处理"模式,第 2547 行):

foreach (var prcId in procIds)
{
    var tt = await GrabImageEx(...);         // 采图(同步等待)
    await ExecuteProcedureAsync(...);        // ToolBlock.Run() → 阻塞(同步等待)
    // ↑ 上一个 ToolBlock 完全结束才进入下一个循环
}

分支 BPhotosUse == false,即"全部采图后批量处理"模式,第 2577 行起):

  • 图像采集被并发化(Task.WhenAll)✅
  • 但 ToolBlock 执行仍然串行 ❌:

    foreach (var item in ImageTaskResults)
    {
    await ExecuteProcedureAsync(...);   // 一个一个串行执行!
    }
    

🟡 根因 4:countdown.Wait() 同步阻塞线程池线程

Management.cs 第 ~2650 行

bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));

这会同步阻塞当前线程池线程最长 5 秒。如果此线程正在处理 PLC 回调,后续 PLC 信号处理全被延迟。


3. 完整调用路径图

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 后台线程:

/// <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 级别做串行化保护。

方案 B(P1):多 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);

⚠️ 注意:需要确保 AllResultlockResultsAll 等共享集合的线程安全性(当前已使用 ConcurrentBag,应该可以)。

方案 C(P2):VPP 加载异步化

ProductService.csLoadProductAsync 中确保反序列化在后台线程:

prc.ToolBlock = await Task.Run(() =>
    _vppCache.GetOrAdd(prcPath, path =>
        CogSerializer.LoadObjectFromFile(path) as CogToolBlock));

同时将所有同步加载路径(Initialize, SetCurrentProduct, LoadLastProduct)改为异步调用。

方案 D(P3):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;
}

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.Waitawait 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 实施概览

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(推荐路径)

代码清单

// 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 行区域)*:

else
{
    // 3. 运行视觉工具
    LogHelper.WriteLogInfo("开始运行视觉工具");
    procedure.ToolBlock.Run();      //运行ToolBlock

    if (procedure.ToolBlock.RunStatus.Result == CogToolResultConstants.Accept)
    {
        // ... 结果处理 ...
    }
}

*修改后*:

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 awaitExecutePhoto2
ExecutePhotoGetSinglePoint RemoteCommandService.cs 3037 await
第二处 ExecutePhotoGetSinglePoint1 RemoteCommandService.cs 3126 await
ExecutePhotoGetSinglePointWithImage RemoteCommandService.cs 3217 awaitExecutePhotoEx

这些调用方全部已经是 async Task 方法,只需在调用处加 await 即可,改动量很小。


Step 3(P0 后续):编译验证 + 本地冒烟测试

编译验证

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 循环

*修改前*:

_ = 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);
        }
        // ...
    }
});

*修改后*:

_ = 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.LoadObjectFromFileVisionProHelper.LoadVisionProObjectAsync
5.2 ~597 LoadProductAsync(Guid id) _vppCache.GetOrAdd 回调中的同步加载改为使用 VisionProHelper
5.3 ~481 SaveProduct(ProductModel p, string filePath) CogSerializer.LoadObjectFromFile / CogSerializer.SaveObjectToFileVisionProHelper 异步方法

详细修改:5.2 示例

*修改前(LoadProductAsync,约第 564~600 行)*:

prc.ToolBlock = _vppCache.GetOrAdd(prcPath, path =>
{
    return CogSerializer.LoadObjectFromFile(path) as CogToolBlock;
});

*修改后*:

// 异步加载 VPP,避免同步 I/O 阻塞调用线程
prc.ToolBlock = await _vppCache.GetOrAddAsync(prcPath, async path =>
{
    return await VisionProHelper.LoadVisionProObjectAsync<CogToolBlock>(path);
});

⚠️ 注意:需要使用支持异步回调的 GetOrAddAsync 扩展方法,或自行实现。若 ConcurrentDictionary 原生不支持,可改为带锁的 Dictionary + 异步判断:

if (!_vppCache.TryGetValue(prcPath, out var cached))
{
    cached = await VisionProHelper.LoadVisionProObjectAsync<CogToolBlock>(prcPath);
    _vppCache.TryAdd(prcPath, cached);
}
prc.ToolBlock = cached;

Step 6(P3):修改 Management.cscountdown.Wait() 改为异步

涉及文件TeamAAS-VM/Core/Management.cs

修改位置:约第 2650 行

*修改前*:

bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));

*修改后*:

// 异步等待 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 中添加配置开关:

    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 个工作日)