项目刚上线,5台PLC,串行轮询,一轮采集800毫秒。
操作工反馈顺畅,数据实时刷新,领导看了也点头——挺好,没啥问题。
然后设备加到了20台。
代码一行没改。但一轮轮询从800毫秒直接飙到3.2秒。操作工点完确认,屏幕纹丝不动,又点了一下——这次触发了两次提交。
再后来,50台。
通讯超时满天飞,采集服务疯狂丢数据,MES里的数据断断续续,完全不可用。
代码没改过,Bug排查了三天三夜,一行问题都没有。
真正的凶手,不是代码——是采集模型本身。
设备扩到20台,代码一行没动。
但一轮轮询从800毫秒直接飙到3.2秒。
3.2秒是什么概念?操作工点完确认键,屏幕纹丝不动——他以为没点着,又点了一下。结果两次都触发了,系统里冒出两条重复记录。
类似的场景在车间里反复上演:按钮多按一次、阀门多开一次、批次号重复一次……每一次"重复提交",都是通讯延迟埋下的雷
直到设备扩到50台,问题彻底炸了。
PLC通讯大面积超时,采集服务像漏水的筛子,数据一条接一条地丢。产线上操作工看着屏幕上的空白,MES系统里全是断断续续的残缺记录——这批数据,完全不可用。
团队花了三天逐行排查,代码翻了个底朝天,结论是:没有Bug。
问题从来不在代码里。
罪魁祸首是串行轮询这个采集模型本身——它的基因里就写着"设备越多,我越慢"。5台时岁月静好,50台时先天不足。
串行轮询 = 用最慢的那台设备,拖垮整个系统。设备越加越多,延迟线性增长,这是顺序轮询的天花板。 假设单台 PLC 平均响应 160ms,串行轮询总耗时 = 设备台数 ×160ms:
5 台 → 5 × 160ms = 800ms,采集周期短,系统响应流畅,完全满足现场使用;20 台 → 20 × 160ms = 3.2s,操作交互延迟明显,现场操作工频繁反馈卡顿;50 台 → 50 × 160ms = 8s,系统采集效率严重不足。 理论计算还只是"最理想情况"。实际工况下,只会更糟:
现场网络抖动,响应时间忽快忽慢
老旧PLC或远距离设备,响应天然慢半拍
通讯超时后程序自动重试,又多等一轮
最致命的是——串行队列是"一个卡,全停"。
只要有一台PLC卡顿2秒,后面所有设备都得干等着。50台设备,一台掉链子,整条链路全部被拖慢。就像50个人排队过安检,一个人翻包翻了半天,后面49个人只能干站着。
说白了,串行轮询是十年前单线程时代的产物。
那时候现场就几台设备,排队读一遍,绰绰有余。但现在呢?几十台PLC同时要数据,几十路信号要实时刷新——你还在一台一台等?
这套模型的设计初衷,就不是为并发而生的。
设备规模上去了,底层采集模型不跟着升级,崩只是时间问题。
最直接的优化:串行改并发。
串行是一台一台排队读,20台等20遍;并发是所有PLC同时发请求,谁先回来谁先处理。总时间不再是20台的总和,而是取决于最慢那台——就像20个窗口同时开饭,你只需要等最慢那个打饭师傅。
// 20 台 PLC,一台一台等,总时间 = 所有台的总和foreach (var plc in plcs){ var data = await plc.ReadAsync(); Process(data);}
// 20 台同时发请求,总时间 = 最慢那台的时间var tasks = plcs.Select(async plc =>{ var data = await plc.ReadAsync(); Process(data);});await Task.WhenAll(tasks);
改造后效果立竿见影:20台PLC的采集延迟从3.2秒直降到约200ms。但别急着高兴——并发不是万能的,有两个坑踩一个就翻车。
坑一:不能无限并发
50台PLC同一秒全发请求,网络瞬间峰值飙到峰值,交换机扛不住直接丢包——你以为在提速,实际上在制造拥塞。
必须用信号量限制并发数:
坑二:老PLC扛不住高并发
并发开得越大越好?天真了。
部分老款PLC、串口转TCP的转换器,处理能力极其有限。你这边10个请求同时打过去,它接不住——轻则响应变慢,重则直接卡死、自动重启。你以为是网络问题,查半天,最后发现是设备本身被"并发"打懵了。
落地建议: 并发数从5开始逐步往上加,边加边观察设备状态。现场设备越老、型号越杂,并发上限越低。切勿一上来就拉满20、50——那不是优化,是压力测试。
不少项目有个通病:所有测点统一采集周期,一视同仁。
这是错的,也是浪费。
报警信号要100ms响应,温度趋势10秒刷一次都够用。你把它们塞进同一个循环,高频在等低频,低频在拖高频——所有人都被拉到了最慢那个节奏
就像公司里,紧急邮件要秒回,月度报表可以慢慢写。但老板非要你每封邮件都按写报表的速度来处理——效率从哪来?
// 三组测点,三个独立循环,互不干扰var highPriority = points.Where(p => p.Level == 1);var midPriority = points.Where(p => p.Level == 2);var lowPriority = points.Where(p => p.Level == 3);_ = Task.Run(() => PollLoopAsync(highPriority, intervalMs: 100, ct));_ = Task.Run(() => PollLoopAsync(midPriority, intervalMs: 500, ct));_ = Task.Run(() => PollLoopAsync(lowPriority, intervalMs: 5000, ct));
实际项目里,分级采集能让轮询总压力下降 50%~80%。因为大多数测点都是低频的,高频轮询只针对少数关键信号。
你会发现一个现实:大部分采集到的数据,和上一次相比没有变化。温度还是 23.1,压力还是 47.3,设备还在运行。但你还是在每个周期都写入 MES,既浪费资源,也增加了数据库的压力。
只处理"有意义的变化":
// 变化超过阈值才写入 MESif (Math.Abs(newValue - lastValue) > threshold){ WriteToMes(newValue); lastValue = newValue;}// 或者直接接入上一篇的 DataQualityChecker// Frozen 状态 = 值没有变化,不写入var quality = checker.Check(newValue);if (quality == DataQuality.Good) WriteToMes(newValue);
变化检测还有一个好处:上一篇的数据质量检测天然集成进来了——值冻结就是"没有有意义的变化",一套逻辑同时解决两个问题。
完整调度器:三个方法合在一起
public class PlcPollingScheduler{ private readonly SemaphoreSlim _semaphore; private readonly int _intervalMs; public PlcPollingScheduler(int maxConcurrency = 10, int intervalMs = 200) { _semaphore = new SemaphoreSlim(maxConcurrency); _intervalMs = intervalMs; } /// 并发轮询一组 PLC,内置变化检测和数据质量过滤 public async Task PollOnceAsync( IEnumerable<plcpoint> points, CancellationToken ct) { var tasks = points.Select(async point => { await _semaphore.WaitAsync(ct); try { double value; try { value = await point.Device.ReadAsync(point.Address, ct); } catch (Exception ex) { // 单台读取失败,记录日志,不影响其他设备 Console.WriteLine($"[读取失败] {point.Name}: {ex.Message}"); return; } // 数据质量检测(接上一篇 DataQualityChecker) var quality = point.Checker.Check(value); if (quality != DataQuality.Good) return; // 变化检测:变化超过阈值才写入 if (Math.Abs(value - point.LastValue) < point.ChangeThreshold) return; point.LastValue = value; await point.MesWriter.WriteAsync(point.Name, value); } finally { _semaphore.Release(); } }); await Task.WhenAll(tasks); } /// 持续轮询循环 public async Task RunAsync( IEnumerable<plcpoint> points, CancellationToken ct) { while (!ct.IsCancellationRequested) { var sw = Stopwatch.StartNew(); await PollOnceAsync(points, ct); sw.Stop(); // 扣除已花时间,保证轮询间隔稳定 var remaining = _intervalMs - (int)sw.ElapsedMilliseconds; if (remaining > 0) await Task.Delay(remaining, ct) .ContinueWith(_ => { }); } }}// 使用:三组分级,各自独立跑var highScheduler = new PlcPollingScheduler(maxConcurrency: 10, intervalMs: 100);var midScheduler = new PlcPollingScheduler(maxConcurrency: 10, intervalMs: 500);var lowScheduler = new PlcPollingScheduler(maxConcurrency: 5, intervalMs: 5000);_ = highScheduler.RunAsync(highPriorityPoints, ct);_ = midScheduler.RunAsync(midPriorityPoints, ct);_ = lowScheduler.RunAsync(lowPriorityPoints, ct);
---
三个方法组合的实际效果
20 台 PLC,优化前延迟稳定在 3 秒以上。加了并发轮询、分级采集、变化过滤三层之后:
并发轮询:3.2s → ~200ms分级采集:轮询总压力下降 ~70%变化过滤:MES 写入量下降 ~60%最终延迟:稳定在 150ms ~ 300ms 采集慢,不是因为你写得慢。而是你还在用串行思维解决并发问题。模型换了,延迟自然就下去了。
</plcpoint></plcpoint>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!