"采集周期一缩短,界面就开始一顿一顿的"——这种卡顿九成不是界面控件的问题,而是采集、算报警、写数据库、刷界面挤在同一条线程里排队。数据库一慢,采集周期就被拖着变慢;界面一刷新,采集就得等。解法是把链路拆开:采集线程只管收,收完丢进队列,入库和刷界面各开消费者慢慢消化。这套生产者-消费者管道是我们所有上位机项目的标准骨架,这篇完整讲一遍。
为什么不能一条线程干到底?
先看一条典型的"单线程流水线"每圈要干的事:读 300 个点位(约 50ms)→ 逐点判断报警 → 拼 INSERT 语句写库(约 30~200ms,看磁盘脸色)→ 更新 40 个 Label 和表格。任何一环变慢,整个周期就变慢,而且数据库写入耗时是最不稳定的——杀毒软件扫一下、机械盘抖一下,单条 INSERT 能从 5ms 飙到 300ms,采集周期跟着坐过山车。
拆成管道之后,各环节只通过队列交换数据,互不阻塞:
▲ 关键原则:生产者永远不等待消费者。采集线程入队后立即进入下一轮,数据库再慢也只影响"数据晚几秒落库",不影响实时性。
队列选什么?有界是底线
C# 里能用的线程安全队列有好几个,选型其实不纠结:
| 容器 | 特点 | 用不用 |
|---|---|---|
| BlockingCollection<T> | 自带阻塞式 Take/GetConsumingEnumerable,支持有界容量,CompleteAdding 优雅关闭 | 默认选择,管道场景就是为它设计的 |
| ConcurrentQueue<T> | 无锁无界,入队出队不阻塞 | 单独用要自己写等待和限流,一般作底层容器 |
| Channel<T> | 异步友好,背压策略丰富,适合 async 体系 | 新项目用 async 全链路时可选,WinForm 老项目 BlockingCollection 更顺手 |
| lock + Queue | 手写锁 | 能用但容易写出死锁和忙等,不推荐 |
队列必须有界。无界队列在数据库持续变慢时会无限积压,内存越涨越高,最后 OutOfMemory 整个程序崩掉——这比丢几秒数据严重得多。有界队列把问题控制在"满了怎么办"这一个决策点上。
入库别一条一条写:攒批是十倍级优化
单条 INSERT 的开销大头是事务提交和磁盘往返,攒成一批写,吞吐量能上一个数量级。消费者代码骨架:
private void DbConsumer() // 独立线程 { var batch = new List<Snapshot>(200); var sw = Stopwatch.StartNew(); foreach (var item in _queue.GetConsumingEnumerable(_cts.Token)) { batch.Add(item); // 攒够200条 或 距上次写库超过1秒,就落一批 if (batch.Count >= 200 || sw.ElapsedMilliseconds > 1000) { Flush(batch); // SqlBulkCopy 或单事务多行INSERT batch.Clear(); sw.Restart(); } } if (batch.Count > 0) Flush(batch); // 收尾:停机前把余量写完 } private void CollectProducer() // 采集线程 { while (!_cts.IsCancellationRequested) { var snap = ReadAllPoints(); // 只读PLC,别的不干 if (_queue.Count > _queue.BoundedCapacity * 0.8) RaiseQueueHighAlarm(); // 水位预警 if (!_queue.TryAdd(snap, 0)) // 满了不阻塞采集 Interlocked.Increment(ref _dropped); _latest = snap; // 供界面定时取用 WaitUntilNextCycle(); } }
两个容易忽略的点:一是"条数 + 时间"双条件触发落批——只按条数,低产时段数据会迟迟不落库,断电就丢了;只按时间,高峰期又攒不够批。两个条件取先到者。二是程序退出时要 CompleteAdding 并把队列余量 Flush 完,不然每次停机都丢最后几秒数据,追溯时对不上账。
界面刷新:别订阅每一条,定时取最新
采集 100ms 一轮,界面没必要也不应该每轮都刷——人眼分辨不出来,控件重绘还会吃 CPU。我们的做法:界面开一个 500ms 的 Timer,每次触发时读取 _latest 最新快照统一刷新。中间被跳过的帧对显示毫无损失,界面负载降为原来的五分之一。报警弹窗和声音走事件通道实时推送,不受刷新节流影响——该快的快,该省的省。
收尾:一张参数表,照着配
| 参数 | 典型值 | 说明 |
|---|---|---|
| 队列容量 | 5000 帧 | 约等于"消费端最慢一次卡顿 × 采集频率"的 3 倍余量 |
| 批量大小 | 200 条 | 机械盘可降到 100,SSD 可到 500 |
| 落批超时 | 1000 ms | 保证低产时数据延迟可控 |
| 界面刷新 | 500 ms | 趋势曲线可单独 1s |
| 水位预警线 | 80% | 触发警告级报警并记日志 |
管道这套东西不神秘,本质就是"谁也别等谁,队列做缓冲,满了有预案"。如果你的上位机现在还是采集入库刷界面一条龙,建议先做最小改造:把写数据库挪到独立线程加个队列——通常这一步做完,界面卡顿就消失大半。
