1.Volatile.Read是否是问题原因
Volatile.Read是一种“带内存可见性保证的读取”。
Action<int,string>?handler=Volatile.Read(ref_faultRunLightRedSolidHandler);它主要保证:
- 其他线程刚通过
Interlocked.Exchange注册的新处理器能够被及时看见。 - 编译器和 CPU 不会随意重排这次读取前后的内存操作。
- 读取本身不会加锁,也不会等待。
- 不会启动线程,也不会执行 PLC 操作。
当前代码是:
Interlocked.Exchange(ref_faultRunLightRedSolidHandler,handler);配合:
Volatile.Read(ref_faultRunLightRedSolidHandler)这是合理的线程安全写法。Volatile.Read只负责取出委托,真正耗时的是后面的:
handler.Invoke(station,reason);因此它不是 UI 卡顿原因。
但DataStorageError和PublishStationLog都存在同一个设计问题:它们在当前调用线程同步执行故障灯处理器。
DataStorageErrorPublishStationLog
如果调用线程是 UI 线程,就会直接卡住 UI。
2. 针对 PLC 断电的最小优化计划
建议只做以下几项,不需要大规模重构。
第一项:故障灯动作改为后台执行
PublishStationLog和DataStorageError不要直接执行:
handler.Invoke(station,reason);改成调用一个统一的后台投递方法,例如:
QueueFaultRunLightAction(station,reason);该方法内部使用线程池执行,并捕获异常:
ThreadPool.QueueUserWorkItem(_=>{try{handler(station,reason);}catch(Exceptionex){DiagnosticError(station,$"故障报警写灯异常:{ex.Message}");}});这样 PLC 即使等待 20 秒,也只会占用后台线程,不会阻塞 UI 线程。
建议增加“每站同时只允许一个写灯任务”,避免 PLC 断电时大量故障不断创建后台任务。
第二项:写灯前先判断 PLC 连接状态
在WriteFaultRunLightRedSolid中增加:
if(plc==null||!plc.IsConnected){LogHelper.DiagnosticError(stationIndex,$"故障报警未能写灯:辅控PLC未连接。原因={reason}");return;}这可以避免已明确断线时仍然发起写操作。
但要注意:连接状态可能在检查后立即断开,因此仍必须保留写入失败处理。
第三项:红灯写失败后立即停止后续写入
当前代码即使红灯写失败,仍然继续写绿灯和黄灯,导致一次故障最多等待三次超时。
建议改成:
intresult=plc.WriteTowerRedSolid("红灯常亮");if(result!=0){LogHelper.DiagnosticError(stationIndex,$"故障报警写RUN红灯失败,返回码={result}。原因={reason}");return;}plc.WriteTowerGreenOff("异常之后关闭绿灯");plc.WriteTowerYellowSolid("异常之后黄灯亮");设备已经断电时,三次写入没有实际意义,继续写只会增加等待时间。
第四项:修正红灯命令名称
当前方法名是WriteFaultRunLightRedSolid,但实际调用:
plc.WriteTowerRedBlinking("红灯常亮");这会产生“名称说常亮,实际命令是闪烁”的问题。应该确认后改为:
plc.WriteTowerRedSolid("红灯常亮");推荐执行顺序
- 先把故障灯写入移到后台线程,解决 UI 卡死。
- 增加
IsConnected判断。 - 红灯失败立即返回,避免继续写绿灯、黄灯。
- 增加每站写灯任务去重。
- 修正
WriteTowerRedBlinking与“红灯常亮”的语义不一致。
这样既保留了故障日志,也保留了 PLC 恢复后继续写灯的机会,同时不会因为 PLC 断电拖死 UI。