首页 新闻 会员 周边

C#监控PLC数据的问题

0
悬赏园豆:20 [待解决问题]

需求是这样。开发一个类库,该类库核心功能是能够读取、写入、订阅PLC的数据,当用户订阅了某写数据时,如果这些数据发生变化,可以通过事件形式,将变化后的数据发给谁用户。

现在做了一个版本。大概思路如下:

1.定义一个接口(比如叫IDevice),该接口包含,链接,断开,读取,写入,订阅plc数据,以及当订阅的数据发生变化后执行的DataChanged事件。

2.实现IDevice接口,比如叫ModbusTcpDevice,该类实现了IDevice。并且ModbusTcpDevice中,缓存用户订阅的数据地址和值,再开一个后台线程,一直去读取 用户订阅的数据,把读取到的数据和缓存的数据比较,同一个地址,缓存数据和当前读取的数据不一致,就把新数据记录一下(比如叫 changedValues ),然后再把缓存值更新成当前值。 最后 执行 DataChanged 事件,传入刚才记录的变化值 (changedValues )。

由此联想的问题:
当一个程序链接较少的PLC 时,应该没什么问题。 如果要链接上百个甚至更多PLC时,每一个IDevice对象都有一个后台线程去轮询,会不会导致线程池爆炸呢?或者引发其他问题。

愤怒的小辣椒的主页 愤怒的小辣椒 | 初学一级 | 园豆:186
提问于:2026-06-26 17:13
< >
分享
所有回答(4)
0

换成Task就可以 ,20万Task,只要不涉及到IO,没问题。

需要格局 | 园豆:2267 (老鸟四级) | 2026-06-27 11:56

感谢您的回复。会涉及到I/O,从PLC 读取数据会有网络的I/O

支持(0) 反对(0) 愤怒的小辣椒 | 园豆:186 (初学一级) | 2026-06-29 08:53

@愤怒的小辣椒: 网络 IO没问题的,只要不涉及到磁盘IO就没问题的,我公司的IOT系统12万的设备都没问题的,Task是几万的

支持(0) 反对(0) 需要格局 | 园豆:2267 (老鸟四级) | 2026-07-01 11:12

@需要格局: 我现在就是task的。相当于一个设备一个后台的tasi在一直循环读数据,然后发现订阅的数据变了,就触发datachange时间。如果同时几百或者更多设备,也就意味着几百或者更多task ,没问题吗?

支持(0) 反对(0) 愤怒的小辣椒 | 园豆:186 (初学一级) | 2026-07-01 13:08

@愤怒的小辣椒: 没问题的。只要Task中没有 磁盘IO,没问题。

支持(0) 反对(0) 需要格局 | 园豆:2267 (老鸟四级) | 2026-07-01 13:34
0

几百个也不是问题呀. 可以考虑改成 一个主管理(timer)线程, 定时批量触发PLC读取.
IDeviceManager
_devices
--> add_device(IDevice device)
--> read_callback(fuction (device, data)[] frame)

--> _trigger_read() --> for _devices
--> read_callback

大致表达意思.

czd890 | 园豆:14830 (专家六级) | 2026-06-29 18:24

那如果其中一个 device 掉线 或者网络延迟很长,其他的是不是有受到影响了呢 ?

支持(0) 反对(0) 愤怒的小辣椒 | 园豆:186 (初学一级) | 2026-06-30 13:10
0

队列行不行?

  1. 全局队列,将变化的设备、值等信息形成结构,当变化发生后放入该队列
  2. 开个线程去试着出队,有值的话通过task去发送消息,调用回调函数,因为task是异步的,不阻塞主线程。
    我不太懂这个,大致思路是这个,可能有问题就是数据多了发不及,但是我感觉成百上千并发应该不是问题
echo_lovely | 园豆:1699 (小虾三级) | 2026-08-13 14:24
0

你的判断方向是对的——这个设计在几十台以内没问题,上百台就会撞上瓶颈,但"线程池爆炸"这个说法需要修正一下,真正的痛点在三个不同层面。我先把诊断说清楚,再给改造建议。

一、先修正诊断:你说的"线程池爆炸"分两种情况

实现方式 真实后果
每个设备 new Thread() 独立后台线程 不是线程池爆炸,而是纯浪费:每线程约 1MB 栈 + 内核对象,100 台 ≈ 上百 MB 空耗内存;线程 99.9% 时间阻塞在 Socket.Receive 上,纯 I/O 等待,白占资源
Task.Run / ThreadPoolwhile(true){ 阻塞读 } 这才是真正的线程池爆炸:长驻阻塞循环吃光池内 worker,池饥饿后 CLR 大约每 0.5~1s 注入一个新线程,高并发下能滚到几千个线程,还会拖累进程内其他异步任务

所以结论是:"每个设备一个轮询线程"是经典反模式,但不是线程数量这一个问题。

二、真正的三个瓶颈(按重要性排序)

1. 协议本身不支持订阅,Modbus 只能轮询,这是根因
你的思路没错——Modbus TCP 没有服务端推送机制,变化检测只能靠"定时读 + 比对缓存"。但轮询可以做得极省:按地址逐个读是浪费,应该按连续寄存器块一次读回(比如一次帧读 64~128 个寄存器),再和缓存 diff。这一条就能把你的请求量降一个数量级。

2. PLC/网关的连接数上限(比线程更早卡死你)
很多 Modbus TCP 从站/网关只允许少量并发连接(常见 1~4 个 socket)。上百台设备各开一个 socket 通常没问题,但如果一个网关下挂多个从站、或者设备本身连接数限制很严,你遇到的第一个问题不是线程,而是连接被拒。这个要在架构里考虑连接池/连接复用。

3. 线程模型本身不scale
线程数=设备数,意味着无法集中做:请求合并、轮询频率自适应、故障隔离、超时背压。

三、改造方案对比

方案 线程模型 扩展性 复杂度 适用
A. 中央轮询引擎(推荐) 每协议一个调度线程(定时轮/Timer),内部 async I/O 好,单线程驱动数百 socket Modbus 这种只能轮询的协议
B. 纯异步订阅引擎 零常驻线程,全 async/await + CancellationToken 最好,可上千 你库的长期形态
C. 固定大小工作池 min(设备数, 小N) 个 worker 复用 某些 PLC 库只能同步 I/O 时的兜底
D. 协议原生订阅 事件驱动 最优 取决于协议 OPC UA / S7 / ADS 有推送能力,但Modbus 没有

你的 IDevice 接口本身(连接/断开/读写/订阅 + DataChanged 事件)设计得没问题,别动它。要改的是实现层。

四、建议的新架构(三层)

IDevice(公共API,保持不变)
   │  订阅/退订/读写
   ▼
SubscriptionEngine(每协议一个,不是每设备一个)
   ├─ 定时轮:Timer Wheel / 调度队列,管理 (设备, 寄存器块, 间隔)
   ├─ async I/O 读块 → 和缓存 diff → 收集 changedValues
   └─ 触发 IDevice.DataChanged(注意线程安全和事件封送)
   │
   ▼
DevicePool / 连接管理(连接复用、断线重连、超时隔离)

几个关键点:

  • 批量块读:订阅时把相邻地址聚合成连续块,一块一次读回,whole-cache diff 后按地址逐个触发事件——你已经有的 changedValues 聚合思路是对的,保留。
  • 单线程调度器最怕一个慢设备拖垮全部:给每个设备/请求加超时和失败计数,超限的降级或隔离,别让一个掉线设备卡住整条轮询链路。
  • 事件线程模型要想清楚:DataChanged 在轮询线程触发,用户 UI 场景可能需要 SynchronizationContext 封送,或者至少文档里写明回调线程。
  • 如果过渡期不想大改,最便宜的方案是:一个共享的 PollScheduler 单线程 + 设备队列,把现在每个设备内部的 while 循环改成"向调度器注册轮询任务",改动量可控。

五、直接回答你的问题

上百个 PLC:会不会炸? —— 用你现在"每设备一线程+逐地址读"的方案,大概率会遇到(线程内存浪费、ThreadPool 饥饿、请求量过大);改不改? —— 改,但不用推翻重来:保留 IDevice 接口,把轮询收拢成每协议一个 async 调度引擎 + 寄存器块批量读,线程数从"设备数"降到"协议数(个位数)",顺带解决连接数限制和请求量问题。这在工业通信库里是成熟套路(HslCommunication 等就是单后台线程驱动多设备)。

如果你愿意,我可以直接帮你把这版改造落地——比如写一个 ModbusSubscriptionEngine 的核心骨架(定时轮 + async 块读 + diff + 事件触发),你拿回去替换现在的后台线程逻辑就行。要做吗?

!!雪莲花!! | 园豆:204 (菜鸟二级) | 2026-09-03 12:06

一看就是用AI写的

支持(0) 反对(0) Lircy | 园豆:208 (菜鸟二级) | 2026-09-11 17:44
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册