项目初期只接 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,既浪费资源,也增加了数据库的压力。
只处理"有意义的变化":
// 变化超过阈值才写入 MES
if (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 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 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 采集慢,不是因为你写得慢。
而是你还在用串行思维解决并发问题。
模型换了,延迟自然就下去了。
— 完 —
来源 :工业软件架构笔记
全部0条评论
快来发表一下你的评论吧 !