初版日期:2026-07-12(首次调查) 修订日期:2026-07-12(日志复查 + 第二轮根因定位 + 修复验证) 涉及文件:
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs、TeamAAS-VM/Services/RemoteCommandService.cs、TeamAAS-VM/Helpers/VisionProHelper.cs、TeamAAS-VM/Core/Management.cs、TeamAAS-VM/Services/ProductService.cs问题:多任务执行时只有相机图像在更新,软件按钮等 UI 操作假死无响应
| 日期 | 版本 | 内容 |
|---|---|---|
| 2026-07-12 | v1 | 初版:定位 ToolBlock.Run() COM 封送、VPP 同步加载、串行执行、countdown.Wait 等 4 个根因 |
| 2026-07-12 | v2 | 日志复查:确认 P0(STA 线程隔离)已落地并验证通过;新增根因 5(RenderUpdate Dispatcher 洪泛)及对应修复 |
C:\Users\teamk\Desktop\新建文件夹\Log\LogInfo\2026-07\2026-07-12.txtC:\Users\teamk\Desktop\新建文件夹\Log\LogError\2026-07\2026-07-12.htmC:\Users\teamk\Desktop\新建文件夹\Log\LogWarn\2026-07\2026-07-12.txt✅ 根因 1(ToolBlock.Run COM 封送)已在当前代码中修复。
日志中可见所有 ToolBlock 执行均通过 VisionProHelper.RunToolBlockAsync 投递到独立 STA 后台线程:
[VP-STA] RunToolBlockAsync ENTER, callingThread=10, callingApartment=MTA, isUIThread=False, newThread=VP-Run-xxx
[VP-STA] STA-Run START, threadId=22, apartment=STA
[VP-STA] STA-Run DONE, threadId=22, elapsedMs=512
[VP-TRACE] ExecutePhotoAsync AFTER RunToolBlockAsync, proc=C1, thread=3, result=Accept
关键证据:callingApartment=MTA、isUIThread=False,新线程 apartment=STA 且 ID 不是 1(UI 线程),确认 ToolBlock.Run() 已不再占用主 UI 线程。
❌ UI heartbeat 在假死期间仍然持续,但 UI 不可交互。
[UI-HEARTBEAT] UI thread alive, queueDepth=0, time=17:23:10.xxx
[UI-HEARTBEAT] UI thread alive, queueDepth=0, time=17:23:11.xxx
...(从未中断)
这说明不是线程死锁或同步阻塞,而是 UI 线程的消息泵虽然运行,但 Dispatcher 工作队列被渲染任务持续占满,用户输入(鼠标点击、按键)长时间得不到调度机会。
❌ 线程 [1] 仍在处理视觉结果和保存。
同一时段日志中 [1] 线程持续输出:
视觉流程[C1]执行结束!耗时:477.6ms
视觉流程[C2]执行结束!耗时:247.8ms
视觉流程[C3]执行结束!耗时:394.6ms
[SAVE-IMAGE] 开始保存流程...
当前产品SN不存在,无法保存图片!
这些操作通过 RenderUpdateNotification 事件 → 订阅端 → Dispatcher.BeginInvoke → Cognex 显示控件更新 + 截图保存,全部回到了 UI 线程。
结论:第一轮修复(STA 线程隔离)生效后,新的瓶颈变为 RenderUpdateNotification 订阅端在 UI 线程上的渲染/保存链路被高频触发导致饥饿。
生产流程中触发固定相机检测时,会按以下顺序执行:
在此过程中可能同时存在多个 ToolBlock 在不同阶段执行,但 UI 完全失去响应。
ToolBlock.Run() 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件状态:✅ 已修复(
VisionProHelper.RunToolBlockAsync),日志验证通过。
调用链(初版,修复前):
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() 均是同步阻塞调用,无一使用后台线程或异步封装。
当前代码中所有 6 处调用已全部替换为 await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock),实现在 TeamAAS-VM/Helpers/VisionProHelper.cs。该方法为每次调用创建独立 STA 后台线程执行 ToolBlock.Run(),日志(见 §0.2)证实 callingApartment=MTA、STA 线程 ID 非 1、UI heartbeat 不受 ToolBlock 执行时段影响。
状态:⚠️ 部分修复。
VisionProHelper.LoadVisionProObjectAsync已提供异步加载能力,但ProductService.Initialize()等同步调用路径尚未全部迁移。
在 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。
状态:⚠️ 待评估。当前仍为串行
foreach+await,但 STA 线程隔离后阻塞影响已大幅降低。
在 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() 同步阻塞线程池线程状态:⚠️ 待修复。日志中可见
countdown wait start, expected=6, current=1记录。
Management.cs 第 ~2650 行:
bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
这会同步阻塞当前线程池线程最长 5 秒。如果此线程正在处理 PLC 回调,后续 PLC 信号处理全被延迟。
RenderUpdateNotification 渲染/保存链路在 UI 线程上形成 Dispatcher 洪泛状态:✅ 已修复。将每帧独立 Dispatcher 投递改为后台入队 + Background 优先级批处理,修复在
ShowVisionRender.xaml.cs。
当 ToolBlock 多任务并发运行且结果密集返回时,每个视觉流程完成都会触发:
RemoteCommandService.ExecutePhotoAsync()
→ _eventAggregator.GetEvent<RenderUpdateNotification>().Publish(showRender) // 每条结果 1~2 次 Publish
→ ShowVisionRender.UpdateRenderModuleSource(render) // 订阅端(后台线程)
→ imageCopy = render.Image.CopyBase(CopyPixels) // 深拷贝图像(后台线程,开销小)
→ gridRender.Dispatcher.BeginInvoke(() => { ... }) // ⚠️ 每帧一次独立 UI 投递
→ recordDisplay.Image = imageCopy; // Cognex 显示控件设图(UI 线程,重)
→ recordDisplay.StaticGraphics.AddList(graphic, ""); // 叠加图形(UI 线程)
→ recordDisplay.Record = record; // 叠加 Record(UI 线程)
→ RequestSaveImage() → disp.CreateContentBitmap() // WinForms 截图(UI 线程,极重)
→ SaveImageAsync() // 磁盘 I/O(已切到后台线程,✅ 不占 UI)
核心矛盾:每个视觉结果都独立 Dispatcher.BeginInvoke(默认优先级 = Normal),当 6 个相机依次在 ~2 秒内完成并投递 6~12 个 UI 更新时,UI 线程的 Dispatcher 队列被这些渲染任务塞满。用户鼠标点击的 Input 优先级消息排在这些 Normal 优先级工作项后面,长时间得不到处理 → 表现为"画面在刷新但点不动按钮"。
[1] 线程(UI 线程)在 17:23:10 ~ 17:23:16 期间密集执行视觉结果处理和保存,而 [UI-HEARTBEAT] 在此期间仍在正常跳动(每 ~500ms),queueDepth=0(消息队列没有积压)——说明死锁不存在,确实是 UI Dispatcher 被占满。
此外,无产品 SN 时仍在做深拷贝、截图等无效 UI 工作后才报"当前产品SN不存在,无法保存图片!",进一步浪费 UI 线程时间。
修改文件:TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs
策略:将"每帧一次独立 UI 投递"改为"后台入队 + UI 线程 Background 优先级批量排空"。
// 新增字段
private readonly ConcurrentQueue<ShowRender> _pendingRenderUpdates = new ConcurrentQueue<ShowRender>();
private int _renderDrainScheduled;
// EnqueueRenderUpdate:后台线程入队(替代旧 UpdateRenderModuleSource)
private void EnqueueRenderUpdate(ShowRender render)
{
// 后台线程深拷贝图像
ICogImage imageCopy = render.Image?.CopyBase(CogImageCopyModeConstants.CopyPixels);
// 入队
_pendingRenderUpdates.Enqueue(new ShowRender { ..., Image = imageCopy, ... });
ScheduleRenderDrain();
}
// ScheduleRenderDrain:UI 线程按 Background 优先级批量排空
private void ScheduleRenderDrain()
{
if (Interlocked.CompareExchange(ref _renderDrainScheduled, 1, 0) != 0)
return; // 已有排空任务在运行,避免重复投递
_ = gridRender.Dispatcher.InvokeAsync(async () =>
{
while (_pendingRenderUpdates.TryDequeue(out var render))
{
ApplyRenderUpdate(render);
// 每处理 2 帧出让 UI 线程,让 Input 优先级消息有机会执行
if (processed % 2 == 0 && !_pendingRenderUpdates.IsEmpty)
await Dispatcher.Yield(DispatcherPriority.Input);
}
}, DispatcherPriority.Background); // ← 关键:Background 优先级,不抢用户输入
}
附加优化:在 RequestSaveImage 入口增加无产品 SN 的提前返回(原本要走到截图阶段才检测),减少无效 UI 工作。
[UI-RENDER] Pending render queue depth=N 可在积压时预警。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 线程执行,且是同步阻塞的。
sequenceDiagram
participant PLC
participant OPC_UA as OPC UA 回调
participant ThreadPool as 线程池
participant VP_STA as STA 后台线程 (VP-Run-xxx)
participant Remote as RemoteCommandService
participant UI_Thread as UI 线程
participant CogDisplay as CogRecordDisplay
participant Disk as 磁盘 I/O
PLC->>OPC_UA: 触发信号
OPC_UA->>ThreadPool: Task.Run → PlcCommandTriggerAsync
ThreadPool->>Remote: ExecutePhotoGetSinglePoint1()
Remote->>VP_STA: await RunToolBlockAsync(toolBlock)
Note over VP_STA: new Thread(ApartmentState.STA)<br/>toolBlock.Run() 在独立线程
VP_STA-->>Remote: 视觉结果
Remote->>Remote: Publish(RenderUpdateNotification)
Note over Remote: 后台线程订阅 EnqueueRenderUpdate<br/>深拷贝图像后入队
Remote-->>UI_Thread: Dispatcher.InvokeAsync(Background)<br/>批量排空 _pendingRenderUpdates
UI_Thread->>CogDisplay: 设图 + Graphic + Record
Note over UI_Thread: 每 2 帧 Yield(Input)<br/>用户点击可插队
UI_Thread-->>Disk: 后台 Task.Run 保存图像
关键变化:
ToolBlock.Run() 在独立 STA 线程执行,不再触碰 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 级别做串行化保护。
TeamAAS-VM/Helpers/VisionProHelper.cs(新增)RunToolBlockAsync(CogToolBlock) — 每次调用创建独立 STA 后台线程ToolBlock.Run() 已替换为 await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock)[VP-STA] STA-Run START, threadId=XX, apartment=STA 中 threadId 均非 1状态:⚠️ 待评估。STA 隔离后串行的阻塞影响已大幅降低,可酌情排期。
在 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 处)、VisionProHelper.cs(新增) |
✅ 已完成 | 中:需验证 ToolBlock 实例线程安全性 | 直接解决 ToolBlock 导致的 UI 假死 |
| P0 | RenderUpdateNotification 渲染批处理 |
ShowVisionRender.xaml.cs |
✅ 已完成 | 低:仅改变调度策略,不改业务逻辑 | 解决多任务渲染导致的 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 阻塞。
gantt
title 修改实施甘特图
dateFormat YYYY-MM-DD
axisFormat %m/%d
section Phase 1 - 基础设施
Step1-创建RunToolBlockAsync工具方法 :done, a1, 2026-07-12, 1d
section Phase 2 - P0 核心修复
Step2-修改RemoteCommandService.cs(6处) :done, a2, after a1, 1d
Step3-编译验证+本地冒烟测试 :done, a3, after a2, 1d
section Phase 2b - P0 渲染修复(新增)
Step3b-RenderUpdate批处理+Background优先级 :done, a3b, after a3, 1d
section Phase 3 - P1 并发优化
Step4-修改Management.cs ToolBlock并发执行 :a4, after a3b, 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
.vpp ToolBlock 的测试产品fix/toolblock-ui-blocking 进行修改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)。
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 | 加 await(ExecutePhoto2) |
ExecutePhotoGetSinglePoint |
RemoteCommandService.cs |
3037 | 加 await |
第二处 ExecutePhotoGetSinglePoint1 |
RemoteCommandService.cs |
3126 | 加 await |
ExecutePhotoGetSinglePointWithImage |
RemoteCommandService.cs |
3217 | 加 await(ExecutePhotoEx) |
这些调用方全部已经是 async Task 方法,只需在调用处加 await 即可,改动量很小。
编译验证:
dotnet build TeamAAS-VM.sln
冒烟测试检查点:
cameraIndex=1),检查:
Out_FixedCameraPickStatus)是否正确写入VP-Run-xxx)是否正常出现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 是安全的。
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 行)*:
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;
Management.cs — countdown.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;
}
}
测试场景矩阵:
| 场景 | 描述 | 预期结果 |
|---|---|---|
| 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-xxxPlcCommandTriggerAsync 入口处已有的 [PLC-TRACE] 日志中对比修改前后的时序如果修改后出现不可预期的问题,可执行以下回滚步骤:
git revert 对应的 commit,或切换到修改前的分支If 仅需禁用 STA 线程化:在 VisionProHelper.RunToolBlockAsync 中添加配置开关:
public static async Task RunToolBlockAsync(CogToolBlock toolBlock)
{
// 紧急回滚开关:从 app.config 读取
if (ConfigurationManager.AppSettings["VisionPro:UseStaThread"] == "false")
{
toolBlock.Run(); // 回退到原始同步调用
return;
}
// ... STA 线程逻辑 ...
}
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| 同一 ToolBlock 实例被多线程并发调用 | 中 | 崩溃或结果错乱 | 确认每个 AOI 点位的 ProcedureModel 使用不同 ToolBlock 实例;若不成立,在 P1 改为按位点串行 |
| STA 线程过多导致资源耗尽 | 低 | 内存增长、调度延迟 | 当前场景最多同时 ~8 个 ToolBlock,STA 线程数可控;若未来扩展,可引入 SemaphoreSlim 限流 |
TaskCompletionSource 未正确释放 |
低 | 内存泄漏 | STA 线程 finally 块中确保 TrySetResult/TrySetException |
| 现有调用方依赖同步返回结果 | 低 | 编译错误 | 所有调用方已在 async Task 上下文中,仅需加 await |
| VisionPro 许可检查在 STA 线程中失败 | 低 | ToolBlock 无法执行 | 保留原始异常信息,通过 tcs.SetException 向上传递 |
| 步骤 | 内容 | 预估工时 |
|---|---|---|
| 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 个工作日) |