C# WinForm ChartControl实时绘制时偶发0值异常问题求助
问题根因
- 异常未拦截后续逻辑:当前代码无论PLC读取是否抛出异常,都会执行添加数据点的逻辑。如果读取PLC失败进入catch块,
atolyevals要么是未初始化的默认值(数组元素全为0),要么是历史脏值,直接取值就会产生0值数据点。 - 跨线程操作UI绑定资源:你使用的
System.Threading.Timer回调运行在后台线程池线程,直接修改绑定到Chart控件的数据源dp1属于非法跨线程UI操作,系统负载升高(比如打开其他应用)时更容易出现数据竞争,读取到脏值。 - 定时器重入问题:如果单次PLC读取+数据处理的耗时超过定时器间隔(比如PLC响应超时、系统资源紧张),多轮定时器回调会并发执行,同时读写
atolyevals、dp1等共享变量,会出现值覆盖、读取到半初始化数组等问题,导致拿到0值。 - 缺失数据合法性校验:读取到PLC值后没有做范围校验,直接写入数据源,异常值不会被过滤。
修复方案
1. 异常分支拦截数据写入
将添加数据点的逻辑移到读取成功的分支下,避免读取失败时写入无效值,同时增加合法性校验:
bool readSuccess = false; try { atolyevals = atolye.ReadHoldingRegisters(2001, 32); readSuccess = true; if (a_s != null) { a_s = null; lineAdd(simdi() + " Atölye PLC bağlantısı sağlandı","g"); } } catch { // 保留原有错误检查、日志记录逻辑 } if(readSuccess) { var currentVal = gazatolyevals[0]; // 按实际PLC量程修改校验范围,过滤异常值 if(currentVal >= 0 && currentVal <= 1000) { // 数据点添加逻辑移到此处 dp1.Add(new DataPoint(DateTime.Now, currentVal)); if (dp1.Count > ViewportPointCount) dp1.RemoveAt(0); } }
2. 修复跨线程UI操作问题
修改UI绑定的数据源必须切换到UI线程执行,WinForm环境下用Invoke封装数据修改逻辑:
if(readSuccess) { var currentVal = gazatolyevals[0]; if(currentVal >= 0 && currentVal <= 1000) { if(this.InvokeRequired) { this.Invoke(new Action(() => { dp1.Add(new DataPoint(DateTime.Now, currentVal)); if (dp1.Count > ViewportPointCount) dp1.RemoveAt(0); })); } else { dp1.Add(new DataPoint(DateTime.Now, currentVal)); if (dp1.Count > ViewportPointCount) dp1.RemoveAt(0); } } }
如果是WPF环境,将上述Invoke替换为Dispatcher.Invoke即可。
3. 防止定时器重入
增加运行状态标记,避免多轮回调并发执行:
// 类级别添加运行状态标记 private volatile bool _isTimerRunning = false; private void Timer_Tick() { // 上一轮任务未执行完则直接跳过本轮 if(_isTimerRunning) return; try { _isTimerRunning = true; // 原有PLC读取、数据处理逻辑放在此处 } finally { // 无论是否出现异常都要重置状态 _isTimerRunning = false; } }
可选优化
如果对实时性要求不高,可直接将System.Threading.Timer替换为WinForm自带的System.Windows.Forms.Timer,该定时器回调天然运行在UI线程,无需额外处理跨线程问题,注意不要在回调中执行过于耗时的逻辑避免卡UI即可。
内容的提问来源于stack exchange,提问作者Mustafa Ozbalci
相关产品推荐
相关产品推荐

