Skip to content

案例 A-01:串口原始数据异步保存

内容类型:真实脱敏案例难度:进阶适合:需要处理串口接收、原始证据和异步保存的开发者阅读时间:约 12 分钟前置:了解 C# 异步和串口接收基础

本案例的关键不是“文件必须用 Channel 写”,而是:在不破坏接收、解析和 Stop 生命周期的前提下,保留可追溯的原始数据。

1. 原始需求

项目希望在协议解析之前保存接收到的原始字节,用于现场问题回放和追溯。

业务约束可以整理成:

text
Record:
保留原始 RX 数据及必要时间/连接上下文

NonGoals:
不改变协议解析语义
不因为 UI 显示需要而裁剪正式保存数据

还需要项目明确一项关键决策:

原始记录失败时,是 AlarmOnly / Advisory 继续采集,还是因为法规/验收要求必须停止?

不能默认所有项目都“保存失败但采集继续”。

2. 先确认接收路径和 Buffer Ownership

System.IO.Ports.SerialPort.DataReceived 事件本身并不会直接把一个业务 byte[] 参数交给应用。项目通常会在回调里读取 BytesToRead / Read(),或者调用自己的接收封装。

因此真正要问的是:

text
Receive Source
→ 谁创建 buffer?
→ buffer 是否复用?
→ 什么时候数据所有权转移?
→ 异步消费者拿到的是独立副本还是共享内存?

只有当项目自己的 buffer 会在下一次读取中复用时,才需要在跨异步边界前复制。

所以更准确的规则是:

跨生命周期/线程边界前,要保证数据所有权清楚;需要时复制,不是看到 DataReceived 就机械 Clone。

3. 为什么“直接 Task.Run 写文件”不一定成立

把每次收到的数据都 Task.Run 一次,可能引入:

  • 多任务并发写同一流,顺序和互斥变复杂;
  • 任务数量在高频输入下持续增长;
  • Stop 时仍有未完成 writer;
  • 异常散落在多个后台任务中;
  • 共享 buffer 所有权不清时出现数据污染。

但这不意味着 Task.Run 在任何项目里都错误。关键是有没有稳定的 writer ownership 和可证明的停止行为。

4. 本案例采用生产者/消费者思路

案例中的目标结构是:

text
Receive Owner
→ immutable/copy buffer(按需要)
→ queue/channel
→ single writer owner
→ file

这样可以把:

  • 设备接收;
  • 协议解析;
  • 原始记录;

分开验证。

Channel 的正确理解

System.Threading.Channels 是一种实现选择,不是规则本身。

  • Bounded Channel 可以配置容量和满载策略,因此能形成可控背压;
  • Unbounded Channel 不会自动解决生产速度长期大于消费速度的问题;
  • 即使用 bounded channel,也必须决定“满了怎么办”:等待、丢弃、报警还是停止。

真正应该写入设计的是 Queue Policy,而不是一句“使用 Channel 即可”。

5. 关闭流程不能只写一个顺序模板

案例中常见的安全顺序是:

text
停止产生新数据
→ 结束/取消接收
→ 通知 writer 不再有新项
→ 按项目策略 drain 或 cancel
→ flush
→ dispose file/session

但要注意两个差异:

Drain

适合“必须尽量把已接收数据落盘”的项目。

Cancel

适合紧急停止、快速退出或数据允许丢弃的项目。

不要把“Stop 一定必须等待所有数据写完”写成绝对规则。真正的行为应由数据重要性、退出时限和安全要求共同决定。

对应方法:线程、设备会话与资源生命周期

6. 保存失败的事件后果要显式定义

可能的项目策略:

场景示例后果
原始日志只是诊断辅助AlarmOnly / Advisory,采集继续
原始记录属于验收必需证据可能阻止开始或停止当前任务
磁盘空间预警Advisory + fail-open
写入过程中真正磁盘满根据数据完整性要求决定继续/停止

所以“写文件异常只记日志、不向上抛”不能被当成通用最佳实践。

7. 原始记录需要什么上下文

只保存裸字节有时不够。现场追溯通常至少考虑:

text
Timestamp
Direction (RX/TX)
Session / Device
Length
Raw Payload
必要时的 Sequence / Command Context

如果协议包含敏感字段,则按项目安全规则脱敏,不把原始协议直接复制到公共资料。

8. 验证按本项目协议和负载选择

不要固定要求所有串口任务都测“半包、粘包、CRC”。这些只在当前协议确实存在相关机制时才有意义。

V2 focused

可以验证:

  • RX 原始数据与文件记录一致;
  • 顺序策略符合要求;
  • writer 异常被正确报告;
  • Stop 后没有旧 writer/session 残留;
  • Fake/Replay 下协议解析结果未改变。

Performance / Long-run

只有存在吞吐或长稳目标时再测:

  • queue depth;
  • write throughput;
  • 内存增长;
  • 72h 等实际要求。

没有做过长期测试就保留 Unverified,不要写“内存稳定”。

HardwarePending

真实设备持续高频输入、断电、驱动行为等场景需要现场条件时,明确保持 HardwarePending

9. 本案例曾采用 Channel,但不要把实现写成规则

案例里使用过类似:

text
receive
→ copy/owned buffer
→ Channel
→ single writer

它解决了当时项目的并发和顺序问题。

但换一个项目,现有成熟 writer、批量缓冲、MemoryMappedFile、数据库批处理或设备 SDK 自带记录机制都可能更合适。

V10 优先复用已有稳定 seam,只有当前实现确实无法满足要求时才引入新抽象。

10. 最值得保留的经验

  1. 先确认 Buffer Ownership。 不把 SerialPort API 和项目自己的缓冲区机制混为一谈。
  2. 接收路径避免不必要的慢操作。 但“哪些操作太慢”用实际负载判断。
  3. Queue Policy 比 Queue 类型更重要。 容量、满载、顺序、丢弃和停止策略必须明确。
  4. 保存失败后果属于业务语义。 不默认 fail-open,也不默认停机。
  5. Stop 需要 owner 和 lifecycle。 不留下旧 writer、重复 session 和悬挂任务。
  6. 原始记录是证据。 最好能进入 Replay,而不只是“保存了一个 bin 文件”。

相关方法

下一步

别来无恙 · C# 上位机 AI 实战站 · 从零到交付 · QQ 群:1016188499