『7x24小时有问必答』
项目初期只接 5 台 PLC,采用串行轮询,一轮采集只要 800 毫秒,系统流畅稳定,一切很顺利现场毫无异议。

后来设备加到 20 台,代码一点没改,但一轮轮询直接涨到 3.2 秒。操作工点完确认键迟迟看不到反馈,以为没触发成功反复点击,频繁出现重复提交的故障。

等到设备扩容到 50 台,问题彻底爆发:大量 PLC 通讯超时,数据采集跟不上节拍,采集服务持续丢数据,产出的数据完全不可用。逐行排查后确认代码不存在 bug,核心症结不在代码,而是串行轮询的采集模型本身承载不了多设备规模。

串行轮询 = 用最慢的那台设备,拖垮整个系统。

设备越加越多,延迟线性增长,这是顺序轮询的天花板。

为什么顺序轮询一定会越来越慢
假设单台 PLC 平均响应 160ms,串行轮询总耗时 = 设备台数 ×160ms:

5 台 → 5 × 160ms =  800ms,采集周期短,系统响应流畅,完全满足现场使用;

20 台 → 20 × 160ms =  3.2s,操作交互延迟明显,现场操作工频繁反馈卡顿;

50 台 → 50 × 160ms =  8s,系统采集效率严重不足。

实际工况下情况会比理论计算更恶劣:现场网络存在抖动,部分老旧或远距离 PLC 响应延迟偏高,通讯超时后程序还会自动重试。一旦某一台 PLC 通讯卡顿 2 秒,整条采集队列都会停滞等待,拖慢全部设备的数据读取。

顺序串行轮询是早期单线程架构的设计思路,面对当下工业现场数十台 PLC 同步采集的并发需求,这套底层采集模型存在根本性瓶颈,天然无法承载大规模设备。

方法一:并发轮询——立竿见影
最直接的优化:把串行改成并发,所有 PLC 同时发请求,总时间取决于最慢那台,不再是所有台的总和。

原来的写法(串行):
// 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 同时发请求,网络瞬间峰值会很高,容易打爆交换机。必须用信号量限制并发数。
// 最多同时请求 10 台,其余排队等var semaphore = new SemaphoreSlim(10);

坑二:老 PLC 扛不住高并发
部分老款 PLC 或者串口转 TCP 的设备,同时收到多个请求会直接卡死或重启。并发数建议从 5 开始测,根据现场实际情况调整,切勿直接设置 20 及以上并发值。

方法二:分级采集——最被低估的优化

不少项目会统一采用相同采集周期读取全部测点,这是错的,也是浪费。

不同数据对实时性要求差异极大:报警信号、操作按钮状态要求100ms快速响应;温度类趋势数据 1 秒采集一次即可满足需求;设备静态运行参数甚至 10 秒更新一次就足够。把它们放在同一个轮询循环里,高频数据在等低频数据,低频数据又在拖高频数据。
高优先级
报警信号、按钮、急停
100ms
中优先级
运行状态、计数器、工步
500ms
低优先级
温度、电流、设备参数
1s~10s
// 三组测点,三个独立循环,互不干扰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>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码